Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-off.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-off.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-off.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-off.png (revision 25381)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-off.png
___________________________________________________________________
Added: mime-type
## -0,0 +1 ##
+image/png
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-on.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-on.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-on.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-on.png (revision 25381)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/autoqueue-on.png
___________________________________________________________________
Added: mime-type
## -0,0 +1 ##
+image/png
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json (revision 25380)
+++ ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json (revision 25381)
@@ -1,289 +1,290 @@
{
"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": "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": "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": "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": "Janwas", "name": "Jan Wassenberg" },
{ "nick": "javiergodas", "name": "Javier Godas Vieitez" },
{ "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": "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": "markcho" },
{ "nick": "MarkT", "name": "Mark Thompson" },
{ "nick": "Markus" },
{ "nick": "Mate-86", "name": "Mate Kovacs" },
{ "nick": "Matei", "name": "Matei Zaharia" },
{ "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": "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": "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": "wraitii", "name": "Lancelot de Ferrière le Vayer" },
{ "nick": "Xentelian", "name": "Mark Strawson" },
{ "nick": "Xienen", "name": "Dayle Flowers" },
{ "nick": "xtizer", "name": "Matt Green" },
{ "nick": "yashi", "name": "Yasushi Shoji" },
{ "nick": "Ykkrosh", "name": "Philip Taylor" },
{ "nick": "Yves" },
{ "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/gui/hotkeys/spec/ingame.json
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/hotkeys/spec/ingame.json (revision 25380)
+++ ps/trunk/binaries/data/mods/public/gui/hotkeys/spec/ingame.json (revision 25381)
@@ -1,164 +1,172 @@
{
"categories": {
"ingame": {
"name": "In-game",
"desc": "In-game hotkeys."
}
},
"mapped_hotkeys": {
"ingame": {
"session.kill": {
"name": "Destroy selected units",
"desc": "Destroy selected units."
},
"session.noconfirmation": {
"name": "Destroy without confirmation",
"desc": "Do not ask confirmation when deleting a building/unit."
},
"session.stop": {
"name": "Stop units",
"desc": "Stop the current action."
},
"session.backtowork": {
"name": "Back to Work",
"desc": "The unit will go back to work."
},
"session.unload": {
"name": "Unload",
"desc": "Unload garrisoned units when a building/mechanical unit is selected."
},
"session.unloadturrets": {
"name": "Unload Turrets",
"desc": "Unload turreted units."
},
"session.leaveturret": {
"name": "Leave Turret Point",
"desc": "Order an entity to leave a turret point."
},
"session.unloadtype": {
"name": "Unload unit type",
"desc": "Modifier to unload all units of type."
},
"session.move": {
"name": "Force move",
"desc": "Modifier to move to a point instead of another action (e.g. gather)."
},
"session.attack": {
"name": "Force attack",
"desc": "Modifier to attack instead of another action (e.g. capture)."
},
"session.attackmove": {
"name": "Attack Move",
"desc": "Modifier to attackmove when clicking on a point."
},
"session.attackmoveUnit": {
"name": "Attack Move (unit only)",
"desc": "Modifier to attackmove targeting only units when clicking on a point."
},
"session.garrison": {
"name": "Garrison",
"desc": "Modifier to garrison when clicking on building."
},
"session.occupyturret": {
"name": "Occupy Turret Point",
"desc": "Modifier to occupy a turret when clicking on a turret holder."
},
"session.autorallypoint": {
"name": "Auto-rally point",
"desc": "Modifier to set the rally point on the building itself."
},
"session.guard": {
"name": "Guard",
"desc": "Modifier to escort/guard when clicking on unit/building."
},
"session.patrol": {
"name": "Patrol",
"desc": "Modifier to patrol a unit."
},
"session.repair": {
"name": "Repair",
"desc": "Modifier to repair when clicking on building/mechanical unit."
},
"session.queue": {
"name": "Queue order",
"desc": "Modifier to queue unit orders instead of replacing."
},
"session.pushorderfront": {
"name": "Push order in front",
"desc": "Modifier to execute this order, then go back to the current order."
},
"session.orderone": {
"name": "Order one unit",
"desc": "Modifier to order only one entity in selection."
},
"session.batchtrain": {
"name": "Batch production",
"desc": "Modifier to train units in batches."
},
"session.massbarter": {
"name": "Mass barter",
"desc": "Modifier to barter bunch of resources."
},
"session.masstribute": {
"name": "Mass tribute",
"desc": "Modifier to tribute bunch of resources."
},
"session.fulltradeswap": {
"name": "Set trade to 100%",
"desc": "Modifier to put the desired trade resource to 100%."
},
"session.deselectgroup": {
"name": "Deselect unit type",
"desc": "Modifier to deselect units when clicking group icon, instead of selecting."
},
"session.rotate.cw": {
"name": "Rotate building clockwise",
"desc": "Rotate building placement preview clockwise."
},
"session.rotate.ccw": {
"name": "Rotate building counterclockwise",
"desc": "Rotate building placement preview anticlockwise."
},
"session.snaptoedges": {
"name": "Toggle snap-to-edge",
"desc": "Modifier to align new structures with nearby existing structure."
},
"session.toggledefaultformation": {
"name": "Toggle default formation",
"desc": "Switch between null default formation and the last default formation used (defaults to 'box')."
},
"session.queueunit.1": {
"name": "Queue 1st unit",
"desc": "Add first unit type to queue."
},
"session.queueunit.2": {
"name": "Queue 2nd unit",
"desc": "Add second unit type to queue."
},
"session.queueunit.3": {
"name": "Queue 3rd unit",
"desc": "Add third unit type to queue."
},
"session.queueunit.4": {
"name": "Queue 4th unit",
"desc": "Add fourth unit type to queue."
},
"session.queueunit.5": {
"name": "Queue 5th unit",
"desc": "Add fivth unit type to queue."
},
"session.queueunit.6": {
"name": "Queue 6th unit",
"desc": "Add sixth unit type to queue."
},
"session.queueunit.7": {
"name": "Queue 7th unit",
"desc": "Add seventh unit type to queue."
},
"session.queueunit.8": {
"name": "Queue 8th unit",
"desc": "Add eighth unit type to queue."
+ },
+ "session.queueunit.autoqueueon": {
+ "name": "Activate auto-queue",
+ "desc": "Turn on Auto-Queue for selected structures."
+ },
+ "session.queueunit.autoqueueoff": {
+ "name": "Deactivate auto-queue",
+ "desc": "Turn off Auto-Queue for selected structures."
}
}
}
}
Index: ps/trunk/binaries/data/mods/public/gui/session/hotkeys/training.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/hotkeys/training.xml (revision 25380)
+++ ps/trunk/binaries/data/mods/public/gui/session/hotkeys/training.xml (revision 25381)
@@ -1,37 +1,47 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 25380)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 25381)
@@ -1,548 +1,570 @@
/**
* @file Contains all helper functions that are needed only for selection_panels.js
* and some that are needed for hotkeys, but not for anything inside input.js.
*/
const UPGRADING_NOT_STARTED = -2;
const UPGRADING_CHOSEN_OTHER = -1;
function canMoveSelectionIntoFormation(formationTemplate)
{
if (formationTemplate == NULL_FORMATION)
return true;
if (!(formationTemplate in g_canMoveIntoFormation))
g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
"formationTemplate": formationTemplate
});
return g_canMoveIntoFormation[formationTemplate];
}
function hasSameRestrictionCategory(templateName1, templateName2)
{
let template1 = GetTemplateData(templateName1);
let template2 = GetTemplateData(templateName2);
if (template1.trainingRestrictions && template2.trainingRestrictions)
return template1.trainingRestrictions.category == template2.trainingRestrictions.category;
if (template1.buildRestrictions && template2.buildRestrictions)
return template1.buildRestrictions.category == template2.buildRestrictions.category;
return false;
}
/**
* Returns a "color:255 0 0 Alpha" string based on how many resources are needed.
*/
function resourcesToAlphaMask(neededResources)
{
let totalCost = 0;
for (let resource in neededResources)
totalCost += +neededResources[resource];
return "color:255 0 0 " + Math.min(125, Math.round(+totalCost / 10) + 50);
}
function getStanceDisplayName(name)
{
switch (name)
{
case "violent":
return translateWithContext("stance", "Violent");
case "aggressive":
return translateWithContext("stance", "Aggressive");
case "defensive":
return translateWithContext("stance", "Defensive");
case "passive":
return translateWithContext("stance", "Passive");
case "standground":
return translateWithContext("stance", "Standground");
default:
warn("Internationalization: Unexpected stance found: " + name);
return name;
}
}
function getStanceTooltip(name)
{
switch (name)
{
case "violent":
return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible");
case "aggressive":
return translateWithContext("stance", "Attack nearby opponents");
case "defensive":
return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location");
case "passive":
return translateWithContext("stance", "Flee if attacked");
case "standground":
return translateWithContext("stance", "Attack opponents in range, but don't move");
default:
return "";
}
}
/**
* Format entity count/limit message for the tooltip
*/
function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
{
if (trainEntLimit == undefined)
return "";
var text = sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), {
"count": trainEntCount,
"limit": trainEntLimit
});
if (trainEntCount >= trainEntLimit)
text = coloredText(text, "red");
for (var c in trainEntLimitChangers)
{
if (!trainEntLimitChangers[c])
continue;
let string = trainEntLimitChangers[c] > 0 ?
translate("%(changer)s enlarges the limit with %(change)s.") :
translate("%(changer)s lessens the limit with %(change)s.");
text += "\n" + sprintf(string, {
"changer": translate(c),
"change": trainEntLimitChangers[c]
});
}
return text;
}
/**
* Format template match count/limit message for the tooltip.
*
* @param {number} matchEntLimit - The limit of the entity.
* @param {number} matchEntCount - The count of the entity.
* @param {string} type - The type of the action (i.e. "build" or "training").
*
* @return {string} - The string to show the user with information regarding the limit of this template.
*/
function formatMatchLimitString(matchEntLimit, matchEntCount, type)
{
if (matchEntLimit == undefined)
return "";
let passedLimit = matchEntCount >= matchEntLimit;
let count = matchEntLimit - matchEntCount;
let text;
if (type == "build")
{
if (passedLimit)
text = sprintf(translatePlural("Could only be constructed once.", "Could only be constructed %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be constructed only once.");
else
text = sprintf(translatePlural("Can be constructed %(count)s more time.", "Can be constructed %(count)s more times.", count), {
"count": count
});
}
else if (type == "training")
{
if (passedLimit)
text = sprintf(translatePlural("Could only be trained once.", "Could only be trained %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be trained only once.");
else
text = sprintf(translatePlural("Can be trained %(count)s more time.", "Can be trained %(count)s more times.", count), {
"count": count
});
}
else
{
if (passedLimit)
text = sprintf(translatePlural("Could only be created once.", "Could only be created %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be created only once.");
else
text = sprintf(translatePlural("Can be created %(count)s more time.", "Can be created %(count)s more times.", count), {
"count": count
});
}
return passedLimit ? coloredText(text, "red") : text;
}
/**
* Format batch training string for the tooltip
* Examples:
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 5"
* buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 10 (2*5)"
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12:
* "Shift-click to train 27 (15 + 12)"
*/
function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
// Don't show the batch training tooltip if either units of this type can't be trained at all
// or only one unit can be trained
if (totalBatchTrainingCount < 2)
return "";
let fullBatchesString = "";
if (buildingsCountToTrainFullBatch > 1)
fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), {
"buildings": buildingsCountToTrainFullBatch,
"batchSize": fullBatchSize
});
else if (buildingsCountToTrainFullBatch == 1)
fullBatchesString = fullBatchSize;
// We need to display the batch details part if there is either more than
// one structure with full batch or one structure with the full batch and
// another with a partial batch
let batchString;
if (buildingsCountToTrainFullBatch > 1 ||
buildingsCountToTrainFullBatch == 1 && remainderBatch > 0)
if (remainderBatch > 0)
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s).");
else
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s).");
else
batchString = translate("%(action)s to train %(number)s.");
return "[font=\"sans-13\"]" +
setStringTags(
sprintf(batchString, {
"action": "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]",
"number": totalBatchTrainingCount,
"fullBatch": fullBatchesString,
"remainderBatch": remainderBatch
}),
g_HotkeyTags) +
"[/font]";
}
/**
* Camera jumping: when the user presses a hotkey the current camera location is marked.
* When pressing another camera jump hotkey the camera jumps back to that position.
* When the camera is already roughly at that location, jump back to where it was previously.
*/
var g_JumpCameraPositions = [];
var g_JumpCameraLast;
function jumpCamera(index)
{
let position = g_JumpCameraPositions[index];
if (!position)
return;
let threshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
let cameraPivot = Engine.GetCameraPivot();
if (g_JumpCameraLast &&
Math.abs(cameraPivot.x - position.x) < threshold &&
Math.abs(cameraPivot.z - position.z) < threshold)
{
Engine.CameraMoveTo(g_JumpCameraLast.x, g_JumpCameraLast.z);
}
else
{
g_JumpCameraLast = cameraPivot;
Engine.CameraMoveTo(position.x, position.z);
}
}
function setJumpCamera(index)
{
g_JumpCameraPositions[index] = Engine.GetCameraPivot();
}
/**
* Called by GUI when user clicks a research button.
*/
function addResearchToQueue(entity, researchType)
{
Engine.PostNetworkCommand({
"type": "research",
"entity": entity,
"template": researchType
});
}
/**
* Called by GUI when user clicks a production queue item.
*/
function removeFromProductionQueue(entity, id)
{
Engine.PostNetworkCommand({
"type": "stop-production",
"entity": entity,
"id": id
});
}
/**
* Called by unit selection buttons.
*/
function changePrimarySelectionGroup(templateName, deselectGroup)
{
g_Selection.makePrimarySelection(templateName,
Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup);
}
function performCommand(entStates, commandName)
{
if (!entStates.length)
return;
if (getCommandInfo(commandName, entStates))
g_EntityCommands[commandName].execute(entStates);
}
function performFormation(entities, formationTemplate)
{
if (!entities)
return;
Engine.PostNetworkCommand({
"type": "formation",
"entities": entities,
"formation": formationTemplate
});
}
function performStance(entities, stanceName)
{
if (!entities)
return;
Engine.PostNetworkCommand({
"type": "stance",
"entities": entities,
"name": stanceName
});
}
function lockGate(lock)
{
Engine.PostNetworkCommand({
"type": "lock-gate",
"entities": g_Selection.toList(),
"lock": lock
});
}
function packUnit(pack)
{
Engine.PostNetworkCommand({
"type": "pack",
"entities": g_Selection.toList(),
"pack": pack,
"queued": false
});
}
function cancelPackUnit(pack)
{
Engine.PostNetworkCommand({
"type": "cancel-pack",
"entities": g_Selection.toList(),
"pack": pack,
"queued": false
});
}
function upgradeEntity(Template, selection)
{
Engine.PostNetworkCommand({
"type": "upgrade",
"entities": selection,
"template": Template,
"queued": false
});
}
function cancelUpgradeEntity()
{
Engine.PostNetworkCommand({
"type": "cancel-upgrade",
"entities": g_Selection.toList(),
"queued": false
});
}
/**
* Set the camera to follow the given entity if it's a unit.
* Otherwise stop following.
*/
function setCameraFollow(entity)
{
let entState = entity && GetEntityState(entity);
if (entState && hasClass(entState, "Unit"))
Engine.CameraFollow(entity);
else
Engine.CameraFollow(0);
}
function stopUnits(entities)
{
Engine.PostNetworkCommand({
"type": "stop",
"entities": entities,
"queued": false
});
}
function unloadTemplate(template, owner)
{
Engine.PostNetworkCommand({
"type": "unload-template",
"all": Engine.HotkeyIsPressed("session.unloadtype"),
"template": template,
"owner": owner,
// Filter out all entities that aren't garrisonable.
"garrisonHolders": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && !!state.garrisonHolder;
})
});
}
function unloadAll()
{
let garrisonHolders = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && !!state.garrisonHolder;
});
if (!garrisonHolders.length)
return;
let ownEnts = [];
let otherEnts = [];
for (let ent of garrisonHolders)
{
if (controlsPlayer(GetEntityState(ent).player))
ownEnts.push(ent);
else
otherEnts.push(ent);
}
if (ownEnts.length)
Engine.PostNetworkCommand({
"type": "unload-all",
"garrisonHolders": ownEnts
});
if (otherEnts.length)
Engine.PostNetworkCommand({
"type": "unload-all-by-owner",
"garrisonHolders": otherEnts
});
}
function unloadAllTurrets()
{
let turretHolders = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && !!state.turretHolder;
});
if (!turretHolders.length)
return;
let ownedHolders = [];
let ejectables = [];
for (let ent of turretHolders)
{
let turretHolderState = GetEntityState(ent);
if (controlsPlayer(turretHolderState.player))
ownedHolders.push(ent);
else
{
for (let turret of turretHolderState.turretHolder.turretPoints.map(tp => tp.entity))
if (turret && controlsPlayer(GetEntityState(turret).player))
ejectables.push(turret);
}
}
if (ejectables.length)
Engine.PostNetworkCommand({
"type": "leave-turret",
"entities": ejectables
});
if (ownedHolders.length)
Engine.PostNetworkCommand({
"type": "unload-turrets",
"entities": ownedHolders
});
}
function leaveTurretPoints()
{
let entities = g_Selection.toList().filter(entity => {
let entState = GetEntityState(entity);
return entState && entState.turretable &&
entState.turretable.holder != INVALID_ENTITY;
});
Engine.PostNetworkCommand({
"type": "leave-turret",
"entities": entities
});
}
function backToWork()
{
Engine.PostNetworkCommand({
"type": "back-to-work",
// Filter out all entities that can't go back to work.
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.hasWorkOrders;
})
});
}
function removeGuard()
{
Engine.PostNetworkCommand({
"type": "remove-guard",
// Filter out all entities that are currently guarding/escorting.
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.isGuarding;
})
});
}
function raiseAlert()
{
Engine.PostNetworkCommand({
"type": "alert-raise",
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && !!state.alertRaiser;
})
});
}
function endOfAlert()
{
Engine.PostNetworkCommand({
"type": "alert-end",
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && !!state.alertRaiser;
})
});
}
+
+function turnAutoQueueOn()
+{
+ Engine.PostNetworkCommand({
+ "type": "autoqueue-on",
+ "entities": g_Selection.toList().filter(ent => {
+ let state = GetEntityState(ent);
+ return state && !!state.production.entities;
+ })
+ });
+}
+
+function turnAutoQueueOff()
+{
+ Engine.PostNetworkCommand({
+ "type": "autoqueue-off",
+ "entities": g_Selection.toList().filter(ent => {
+ let state = GetEntityState(ent);
+ return state && !!state.production.entities;
+ })
+ });
+}
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 25380)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 25381)
@@ -1,1833 +1,1873 @@
/**
* Specifies which template should indicate the target location of a player command,
* given a command type.
*/
var g_TargetMarker = {
"move": "special/target_marker"
};
/**
* Which enemy entity types will be attacked on sight when patroling.
*/
var g_PatrolTargets = ["Unit"];
const g_DisabledTags = { "color": "255 140 0" };
/**
* List of different actions units can execute,
* this is mostly used to determine which actions can be executed
*
* "execute" is meant to send the command to the engine
*
* The next functions will always return false
* in case you have to continue to seek
* (i.e. look at the next entity for getActionInfo, the next
* possible action for the actionCheck ...)
* They will return an object when the searching is finished
*
* "getActionInfo" is used to determine if the action is possible,
* and also give visual feedback to the user (tooltips, cursors, ...)
*
* "preSelectedActionCheck" is used to select actions when the gui buttons
* were used to set them, but still require a target (like the guard button)
*
* "hotkeyActionCheck" is used to check the possibility of actions when
* a hotkey is pressed
*
* "actionCheck" is used to check the possibilty of actions without specific
* command. For that, the specificness variable is used
*
* "specificness" is used to determine how specific an action is,
* The lower the number, the more specific an action is, and the bigger
* the chance of selecting that action when multiple actions are possible
*/
var g_UnitActions =
{
"move":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "walk",
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.move") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("move", target, selection);
return actionInfo.possible && {
"type": "move",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 12,
},
"attack-move":
{
"execute": function(target, action, selection, queued, pushFront)
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
Engine.PostNetworkCommand({
"type": "attack-walk",
"entities": selection,
"x": target.x,
"z": target.z,
"targetClasses": targetClasses,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return isAttackMovePressed() &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack-move", target, selection);
return actionInfo.possible && {
"type": "attack-move",
"cursor": "action-attack-move",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 30,
},
"capture":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"allowCapture": true,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState || !targetState.capturePoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["Capture"]
})
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("capture", target, selection);
return actionInfo.possible && {
"type": "capture",
"cursor": "action-capture",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 9,
},
"attack":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"allowCapture": false,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState || !targetState.hitpoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["!Capture"]
})
};
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.attack") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack", target, selection);
return actionInfo.possible && {
"type": "attack",
"cursor": "action-attack",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 10,
},
"patrol":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "patrol",
"entities": selection,
"x": target.x,
"z": target.z,
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
"queued": queued,
"allowCapture": false,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_patrol",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.canPatrol)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.patrol") &&
this.actionCheck(target, selection);
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_PATROL &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("patrol", target, selection);
return actionInfo.possible && {
"type": "patrol",
"cursor": "action-patrol",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 37,
},
"heal":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "heal",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_heal",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.heal || !targetState ||
!hasClass(targetState, "Unit") || !targetState.needsHeal ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
entState.id == targetState.id) // Healers can't heal themselves.
return false;
let unhealableClasses = entState.heal.unhealableClasses;
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
return false;
let healableClasses = entState.heal.healableClasses;
if (!MatchesClassList(targetState.identity.classes, healableClasses))
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("heal", target, selection);
return actionInfo.possible && {
"type": "heal",
"cursor": "action-heal",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 7,
},
// "Fake" action to check if an entity can be ordered to "construct"
// which is handled differently from repair as the target does not exist.
"construct":
{
"preSelectedActionCheck": function(target, selection)
{
let state = GetEntityState(selection[0]);
if (state && state.builder &&
target && target.constructor && target.constructor.name == "PlacementSupport")
return { "type": "construct" };
return false;
},
"specificness": 0,
},
"repair":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "repair",
"entities": selection,
"target": action.target,
"autocontinue": true,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": action.foundation ? "order_build" : "order_repair",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.builder || !targetState ||
!targetState.needsRepair && !targetState.foundation ||
!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
return {
"possible": true,
"foundation": targetState.foundation
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_REPAIR && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-repair-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.repair") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("repair", target, selection);
return actionInfo.possible && {
"type": "repair",
"cursor": "action-repair",
"target": target,
"foundation": actionInfo.foundation,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 11,
},
"gather":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "gather",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.resourceGatherRates ||
!targetState || !targetState.resourceSupply)
return false;
let resource;
if (entState.resourceGatherRates[targetState.resourceSupply.type.generic + "." + targetState.resourceSupply.type.specific])
resource = targetState.resourceSupply.type.specific;
else if (entState.resourceGatherRates[targetState.resourceSupply.type.generic])
resource = targetState.resourceSupply.type.generic;
if (!resource)
return false;
return {
"possible": true,
"cursor": "action-gather-" + resource
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("gather", target, selection);
return actionInfo.possible && {
"type": "gather",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 1,
},
"returnresource":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "returnresource",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || !targetState.resourceDropsite)
return false;
let playerState = GetSimState().players[entState.player];
if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
{
if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
}
else if (!playerCheck(entState, targetState, ["Player"]))
return false;
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
return false;
let carriedType = entState.resourceCarrying[0].type;
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
return false;
return {
"possible": true,
"cursor": "action-return-" + carriedType
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("returnresource", target, selection);
return actionInfo.possible && {
"type": "returnresource",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 2,
},
"cancel-setup-trade-route":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "cancel-setup-trade-route",
"entities": selection,
"target": action.target,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails || !tradingDetails.type)
return false;
if (tradingDetails.type == "is first" && !tradingDetails.hasBothMarkets)
return {
"possible": true,
"tooltip": translate("This is the origin trade market.\nRight-click to cancel trade route.")
};
return false;
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("cancel-setup-trade-route", target, selection);
return actionInfo.possible && {
"type": "cancel-setup-trade-route",
"cursor": "action-cancel-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 2,
},
"setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "setup-trade-route",
"entities": selection,
"target": action.target,
"source": null,
"route": null,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_trade",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails)
return false;
let tooltip;
switch (tradingDetails.type)
{
case "is first":
tooltip = translate("Origin trade market.") + "\n";
if (tradingDetails.hasBothMarkets)
tooltip += sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
else
return false;
break;
case "is second":
tooltip = translate("Destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
case "set first":
tooltip = translate("Right-click to set as origin trade market");
break;
case "set second":
if (tradingDetails.gain.traderGain == 0)
return {
"possible": true,
"tooltip": setStringTags(translate("This market is too close to the origin market."), g_DisabledTags),
"disabled": true
};
tooltip = translate("Right-click to set as destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
}
return {
"possible": true,
"tooltip": tooltip
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("setup-trade-route", target, selection);
if (actionInfo.disabled)
return {
"type": "none",
"cursor": "action-setup-trade-route-disabled",
"target": null,
"tooltip": actionInfo.tooltip
};
return actionInfo.possible && {
"type": "setup-trade-route",
"cursor": "action-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 0,
},
"occupy-turret":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "occupy-turret",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.turretable || !targetState || !targetState.turretHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
if (!targetState.turretHolder.turretPoints.find(point =>
!point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses)))
return false;
let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
"occupied": occupiedTurrets.length,
"capacity": targetState.turretHolder.turretPoints.length
});
if (occupiedTurrets.length == targetState.turretHolder.turretPoints.length)
tooltip = coloredText(tooltip, "orange");
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_OCCUPY_TURRET && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-occupy-turret-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.occupyturret") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("occupy-turret", target, selection);
return actionInfo.possible && {
"type": "occupy-turret",
"cursor": "action-occupy-turret",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 21,
},
"garrison":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "garrison",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.garrisonable || !targetState || !targetState.garrisonHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.occupiedSlots,
"capacity": targetState.garrisonHolder.capacity
});
let extraCount = entState.garrisonable.size;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.occupiedSlots;
if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
return false;
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GARRISON && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-garrison-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.garrison") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("garrison", target, selection);
return actionInfo.possible && {
"type": "garrison",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 20,
},
"guard":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "guard",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || !targetState.guard || entState.id == targetState.id ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
!entState.unitAI || !entState.unitAI.canGuard)
return false;
return { "possible": true };
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GUARD && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-guard-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.guard") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("guard", target, selection);
return actionInfo.possible && {
"type": "guard",
"cursor": "action-guard",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 40,
},
"collect-treasure":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "collect-treasure",
"entities": selection,
"target": action.target,
"queued": queued,
"autocontinue": true,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_collect_treasure",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.treasureCollecter ||
!targetState || !targetState.treasure)
return false;
return {
"possible": true,
"cursor": "action-collect-treasure"
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("collect-treasure", target, selection);
return actionInfo.possible && {
"type": "collect-treasure",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 1,
},
"remove-guard":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "remove-guard",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.isGuarding)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.guard") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("remove-guard", target, selection);
return actionInfo.possible && {
"type": "remove-guard",
"cursor": "action-remove-guard",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 41,
},
"set-rallypoint":
{
"execute": function(target, action, selection, queued, pushFront)
{
// if there is a position set in the action then use this so that when setting a
// rally point on an entity it is centered on that entity
if (action.position)
target = action.position;
Engine.PostNetworkCommand({
"type": "set-rallypoint",
"entities": selection,
"x": target.x,
"z": target.z,
"data": action.data,
"queued": queued
});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.rallyPoint)
return false;
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities.
if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") ||
!targetState.production ||
!targetState.production.entities.length))
for (let ent in g_Selection.selected)
if (targetState.id == +ent)
return false;
let tooltip;
let disabled = false;
// default to walking there (or attack-walking if hotkey pressed)
let data = { "command": "walk" };
let cursor = "";
if (isAttackMovePressed())
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
data.command = "attack-walk";
data.targetClasses = targetClasses;
cursor = "action-attack-move";
}
if (Engine.HotkeyIsPressed("session.repair") && targetState &&
(targetState.needsRepair || targetState.foundation) &&
playerCheck(entState, targetState, ["Player", "Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState && targetState.garrisonHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "garrison";
data.target = targetState.id;
cursor = "action-garrison";
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.occupiedSlots,
"capacity": targetState.garrisonHolder.capacity
});
if (targetState.garrisonHolder.occupiedSlots >=
targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.turretHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "occupy-turret";
data.target = targetState.id;
cursor = "action-garrison";
let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
"occupied": occupiedTurrets.length,
"capacity": targetState.turretHolder.turretPoints.length
});
if (occupiedTurrets.length >= targetState.turretHolder.turretPoints.length)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.resourceSupply)
{
let resourceType = targetState.resourceSupply.type;
cursor = "action-gather-" + resourceType.specific;
data.command = "gather-near-position";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
if (!targetState.speed)
{
data.command = "gather";
data.target = targetState.id;
}
}
else if (targetState && targetState.treasure)
{
cursor = "action-collect-treasure";
data.command = "collect-treasure-near-position";
if (!targetState.speed)
{
data.command = "collect-treasure";
data.target = targetState.id;
}
}
else if (entState.market && targetState && targetState.market &&
entState.id != targetState.id &&
(!entState.market.naval || targetState.market.naval) &&
!playerCheck(entState, targetState, ["Enemy"]))
{
// Find a trader (if any) that this structure can train.
let trader;
if (entState.production && entState.production.entities.length)
for (let i = 0; i < entState.production.entities.length; ++i)
if ((trader = GetTemplateData(entState.production.entities[i]).trader))
break;
let traderData = {
"firstMarket": entState.id,
"secondMarket": targetState.id,
"template": trader
};
let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
if (gain)
{
data.command = "trade";
data.target = traderData.secondMarket;
data.source = traderData.firstMarket;
cursor = "action-setup-trade-route";
if (gain.traderGain)
tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
sprintf(
trader ?
translate("Gain: %(gain)s") :
translate("Expected gain: %(gain)s"),
{ "gain": getTradingTooltip(gain) });
else
{
disabled = true;
tooltip = setStringTags(translate("This market is too close to the origin market."), g_DisabledTags);
cursor = "action-setup-trade-route-disabled";
}
}
}
else if (targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState && playerCheck(entState, targetState, ["Enemy"]))
{
data.target = targetState.id;
data.command = "attack";
cursor = "action-attack";
}
return {
"possible": true,
"data": data,
"position": targetState && targetState.position,
"cursor": cursor,
"disabled": disabled,
"tooltip": tooltip
};
},
"hotkeyActionCheck": function(target, selection)
{
// Hotkeys are checked in the actionInfo.
return this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
// We want commands to units take precedence.
if (selection.some(ent => {
let entState = GetEntityState(ent);
return entState && !!entState.unitAI;
}))
return false;
let actionInfo = getActionInfo("set-rallypoint", target, selection);
if (actionInfo.disabled)
return {
"type": "none",
"cursor": actionInfo.cursor,
"target": null,
"tooltip": actionInfo.tooltip
};
return actionInfo.possible && {
"type": "set-rallypoint",
"cursor": actionInfo.cursor,
"data": actionInfo.data,
"tooltip": actionInfo.tooltip,
"position": actionInfo.position,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 6,
},
"unset-rallypoint":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "unset-rallypoint",
"entities": selection
});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState ||
entState.id != targetState.id || entState.unitAI ||
!entState.rallyPoint || !entState.rallyPoint.position)
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("unset-rallypoint", target, selection);
return actionInfo.possible && {
"type": "unset-rallypoint",
"cursor": "action-unset-rally",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 11,
},
// This is a "fake" action to show a failure cursor
// when only uncontrollable entities are selected.
"uncontrollable":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"actionCheck": function(target, selection)
{
// Only show this action if all entities are marked uncontrollable.
let playerState = g_SimState.players[g_ViewedPlayer];
if (playerState && playerState.controlsAll || selection.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.identity && entState.identity.controllable;
}))
return false;
return {
"type": "none",
"cursor": "cursor-no",
"tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length)
};
},
"specificness": 100,
},
"none":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"specificness": 100,
},
};
var g_UnitActionsSortedKeys = Object.keys(g_UnitActions).sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness);
/**
* Info and actions for the entity commands
* Currently displayed in the bottom of the central panel
*/
var g_EntityCommands =
{
"unload-all": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
{
if (!entState.garrisonHolder)
continue;
if (allowedPlayersCheck([entState], ["Player"]))
count += entState.garrisonHolder.entities.length;
else
for (let entity of entState.garrisonHolder.entities)
if (allowedPlayersCheck([GetEntityState(entity)], ["Player"]))
++count;
}
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
translate("Unload All."),
"icon": "garrison-out.png",
"count": count,
"enabled": true
};
},
"execute": function()
{
unloadAll();
},
"allowedPlayers": ["Player", "Ally"]
},
"unload-all-turrets": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
{
if (!entState.turretHolder)
continue;
if (allowedPlayersCheck([entState], ["Player"]))
count += entState.turretHolder.turretPoints.filter(turretPoint => turretPoint.entity && turretPoint.ejectable).length;
else
for (let turretPoint of entState.turretHolder.turretPoints)
if (turretPoint.entity && allowedPlayersCheck([GetEntityState(turretPoint.entity)], ["Player"]))
++count;
}
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unloadturrets") +
translate("Unload Turrets."),
"icon": "garrison-out.png",
"count": count,
"enabled": true
};
},
"execute": function()
{
unloadAllTurrets();
},
"allowedPlayers": ["Player", "Ally"]
},
"delete": {
"getInfo": function(entStates)
{
return entStates.some(entState => !isUndeletable(entState)) ?
{
"tooltip":
colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
translate("Destroy the selected units or structures.") + "\n" +
colorizeHotkey(
translate("Use %(hotkey)s to avoid the confirmation dialog."),
"session.noconfirmation"
),
"icon": "kill_small.png",
"enabled": true
} :
{
// Get all delete reasons and remove duplications
"tooltip": entStates.map(entState => isUndeletable(entState))
.filter((reason, pos, self) =>
self.indexOf(reason) == pos && reason
).join("\n"),
"icon": "kill_small_disabled.png",
"enabled": false
};
},
"execute": function(entStates)
{
let entityIDs = entStates.reduce(
(ids, entState) => {
if (!isUndeletable(entState))
ids.push(entState.id);
return ids;
},
[]);
if (!entityIDs.length)
return;
let deleteSelection = () => Engine.PostNetworkCommand({
"type": "delete-entities",
"entities": entityIDs
});
if (Engine.HotkeyIsPressed("session.noconfirmation"))
deleteSelection();
else
(new DeleteSelectionConfirmation(deleteSelection)).display();
},
"allowedPlayers": ["Player"]
},
"stop": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
translate("Abort the current order."),
"icon": "stop.png",
"enabled": true
};
},
"execute": function(entStates)
{
if (entStates.length)
stopUnits(entStates.map(entState => entState.id));
},
"allowedPlayers": ["Player"]
},
"garrison": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.garrisonable ||
entState.garrisonable.holder != INVALID_ENTITY))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
translate("Order the selected units to garrison in a structure or unit."),
"icon": "garrison.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
},
"allowedPlayers": ["Player"]
},
"occupy-turret": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.turretable ||
entState.turretable.holder != INVALID_ENTITY))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.occupyturret") +
translate("Order the selected units to occupy a turret point."),
"icon": "occupy-turret.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_OCCUPY_TURRET;
},
"allowedPlayers": ["Player"]
},
"leave-turret": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.turretable ||
entState.turretable.holder == INVALID_ENTITY ||
!entState.turretable.ejectable))
return false;
return {
"tooltip": translate("Unload"),
"icon": "leave-turret.png",
"enabled": true
};
},
"execute": function(entStates)
{
leaveTurretPoints();
},
"allowedPlayers": ["Player"]
},
"repair": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.builder))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
translate("Order the selected units to repair a structure, ship, or siege engine."),
"icon": "repair.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
},
"allowedPlayers": ["Player"]
},
"focus-rally": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.rallyPoint))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
translate("Focus on Rally Point."),
"icon": "focus-rally.png",
"enabled": true
};
},
"execute": function(entStates)
{
// TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
let focusTarget;
for (let entState of entStates)
if (entState.rallyPoint && entState.rallyPoint.position)
{
focusTarget = entState.rallyPoint.position;
break;
}
if (!focusTarget)
for (let entState of entStates)
if (entState.position)
{
focusTarget = entState.position;
break;
}
if (focusTarget)
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
},
"allowedPlayers": ["Player", "Observer"]
},
"back-to-work": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
translate("Back to Work"),
"icon": "back-to-work.png",
"enabled": true
};
},
"execute": function()
{
backToWork();
},
"allowedPlayers": ["Player"]
},
"add-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState =>
!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
translate("Order the selected units to guard a structure or unit."),
"icon": "add-guard.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
},
"allowedPlayers": ["Player"]
},
"remove-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
return false;
return {
"tooltip": translate("Remove guard"),
"icon": "remove-guard.png",
"enabled": true
};
},
"execute": function()
{
removeGuard();
},
"allowedPlayers": ["Player"]
},
"select-trading-goods": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.market))
return false;
return {
"tooltip": translate("Barter & Trade"),
"icon": "economics.png",
"enabled": true
};
},
"execute": function()
{
g_TradeDialog.toggle();
},
"allowedPlayers": ["Player"]
},
"patrol": {
"getInfo": function(entStates)
{
if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
translate("Patrol") + "\n" +
translate("Attack all encountered enemy units while avoiding structures."),
"icon": "patrol.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_PATROL;
},
"allowedPlayers": ["Player"]
},
"share-dropsite": {
"getInfo": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!sharableEntities.length)
return false;
// Returns if none of the entities belong to a player with a mutual ally.
if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
(isAlly, playerId) => isAlly && playerId != entState.player)))
return false;
return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
{
"tooltip": translate("Press to allow allies to use this dropsite"),
"icon": "locked_small.png",
"enabled": true
} :
{
"tooltip": translate("Press to prevent allies from using this dropsite"),
"icon": "unlocked_small.png",
"enabled": true
};
},
"execute": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (sharableEntities)
Engine.PostNetworkCommand({
"type": "set-dropsite-sharing",
"entities": sharableEntities.map(entState => entState.id),
"shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
});
},
"allowedPlayers": ["Player"]
},
"is-dropsite-shared": {
"getInfo": function(entStates)
{
let shareableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!shareableEntities.length)
return false;
let player = Engine.GetPlayerID();
let simState = GetSimState();
if (!g_IsObserver && !simState.players[player].hasSharedDropsites ||
shareableEntities.every(entState => controlsPlayer(entState.player)))
return false;
if (!shareableEntities.every(entState => entState.resourceDropsite.shared))
return {
"tooltip": translate("The use of this dropsite is prohibited"),
"icon": "locked_small.png",
"enabled": false
};
return {
"tooltip": g_IsObserver ? translate("Allies are allowed to use this dropsite.") :
translate("You are allowed to use this dropsite"),
"icon": "unlocked_small.png",
"enabled": false
};
},
"execute": function(entState)
{
// This command button is always disabled.
},
"allowedPlayers": ["Ally", "Observer"]
- }
+ },
+
+ "autoqueue-on": {
+ "getInfo": function(entStates)
+ {
+ if (entStates.every(entState => !entState.production || !entState.production.entities || entState.production.autoqueue))
+ return false;
+ return {
+ "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueon") +
+ translate("Activate auto-queue for selected structures."),
+ "icon": "autoqueue-on.png",
+ "enabled": true
+ };
+ },
+ "execute": function(entStates)
+ {
+ if (entStates.length)
+ turnAutoQueueOn();
+ },
+ "allowedPlayers": ["Player"]
+ },
+
+ "autoqueue-off": {
+ "getInfo": function(entStates)
+ {
+ if (entStates.every(entState => !entState.production || !entState.production.entities || !entState.production.autoqueue))
+ return false;
+ return {
+ "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueoff") +
+ translate("Deactivate auto-queue for selected structures."),
+ "icon": "autoqueue-off.png",
+ "enabled": true
+ };
+ },
+ "execute": function(entStates)
+ {
+ if (entStates.length)
+ turnAutoQueueOff();
+ },
+ "allowedPlayers": ["Player"]
+ },
};
function playerCheck(entState, targetState, validPlayers)
{
let playerState = GetSimState().players[entState.player];
for (let player of validPlayers)
if (player == "Gaia" && targetState.player == 0 ||
player == "Player" && targetState.player == entState.player ||
playerState["is" + player] && playerState["is" + player][targetState.player])
return true;
return false;
}
/**
* Checks whether the entities have the right diplomatic status
* with respect to the currently active player.
* Also "Observer" can be used.
*
* @param {Object[]} entStates - An array containing the entity states to check.
* @param {string[]} validPlayers - An array containing the diplomatic statuses.
*
* @return {boolean} - Whether the currently active player is allowed.
*/
function allowedPlayersCheck(entStates, validPlayers)
{
// Assume we can only select entities from one player,
// or it does not matter (e.g. observer).
let targetState = entStates[0];
let playerState = GetSimState().players[Engine.GetPlayerID()];
return validPlayers.some(player =>
player == "Observer" && g_IsObserver ||
player == "Player" && controlsPlayer(targetState.player) ||
playerState && playerState["is" + player] && playerState["is" + player][targetState.player]);
}
function hasClass(entState, className)
{
// note: use the functions in globalscripts/Templates.js for more versatile matching
return entState.identity && entState.identity.classes.indexOf(className) != -1;
}
/**
* Keep in sync with Commands.js.
*/
function isUndeletable(entState)
{
let playerState = g_SimState.players[entState.player];
if (playerState && playerState.controlsAll)
return false;
if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
return translate("The entity has to be killed before it can be gathered from");
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
return translate("You cannot destroy this entity as you own less than half the capture points");
if (!entState.identity.canDelete)
return translate("This entity is undeletable");
return false;
}
function DrawTargetMarker(target)
{
Engine.GuiInterfaceCall("AddTargetMarker", {
"template": g_TargetMarker.move,
"x": target.x,
"z": target.z
});
}
function getCommandInfo(command, entStates)
{
return entStates && g_EntityCommands[command] &&
allowedPlayersCheck(entStates, g_EntityCommands[command].allowedPlayers) &&
g_EntityCommands[command].getInfo(entStates);
}
function getActionInfo(action, target, selection)
{
if (!selection || !selection.length || !GetEntityState(selection[0]))
return { "possible": false };
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
let targetState = GetEntityState(target);
let simState = GetSimState();
let playerState = g_SimState.players[g_ViewedPlayer];
// Check if any entities in the selection can do some of the available actions.
for (let entityID of selection)
{
let entState = GetEntityState(entityID);
if (!entState)
continue;
if (playerState && !playerState.controlsAll && !entState.identity.controllable)
continue;
if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
{
let r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
if (r && r.possible)
{
r.entity = entityID;
return r;
}
}
}
return { "possible": false };
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25380)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25381)
@@ -1,2139 +1,2140 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"resourceGatherers": cmpPlayer.GetResourceGatherers(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": cmpPlayer.CanBarter(),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
/**
* Called when the game ends if the current game is part of a campaign run.
*/
GuiInterface.prototype.GetCampaignGameEndData = function(player)
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (Trigger.prototype.OnCampaignGameEnd)
return Trigger.prototype.OnCampaignGameEnd();
return {};
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
if (!ent)
return null;
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList(),
"controllable": cmpIdentity.IsControllable()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval")
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress()
};
let cmpPopulation = Engine.QueryInterface(ent, IID_Population);
if (cmpPopulation)
ret.population = {
"bonus": cmpPopulation.GetPopBonus()
};
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo(),
"isUpgrading": cmpUpgrade.IsUpgrading()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
- "queue": cmpProductionQueue.GetQueue()
+ "queue": cmpProductionQueue.GetQueue(),
+ "autoqueue": cmpProductionQueue.IsAutoQueueing()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
};
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
if (cmpTurretHolder)
ret.turretHolder = {
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (cmpTurretable)
ret.turretable = {
"ejectable": cmpTurretable.IsEjectable(),
"holder": cmpTurretable.HolderID()
};
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
ret.garrisonable = {
"holder": cmpGarrisonable.HolderID(),
"size": cmpGarrisonable.UnitSize()
};
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle()
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked()
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].attackName = cmpAttack.GetAttackName(type);
ret.attack[type].splash = cmpAttack.GetSplashData(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpResistance = Engine.QueryInterface(ent, IID_Resistance);
if (cmpResistance)
ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"health": cmpHeal.GetHealth(),
"range": cmpHeal.GetRange().max,
"interval": cmpHeal.GetInterval(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses()
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetInterval(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure);
if (cmpTreasure)
ret.treasure = {
"collectTime": cmpTreasure.CollectionTime(),
"resources": cmpTreasure.Resources()
};
let cmpTreasureCollecter = Engine.QueryInterface(ent, IID_TreasureCollecter);
if (cmpTreasureCollecter)
ret.treasureCollecter = true;
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep);
if (cmpUpkeep)
ret.upkeep = {
"interval": cmpUpkeep.GetInterval(),
"rates": cmpUpkeep.GetRates()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
{
let auraTemplate = AuraTemplates.Get(name);
if (!auraTemplate)
error("Template " + templateName + " has undefined aura " + name);
else
aurasTemplate[name] = auraTemplate;
}
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
if (!cmpAttackDetection)
return [];
return cmpAttackDetection.GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player);
return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {};
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
let cmpPlayer = QueryPlayerIDInterface(wantedPlayer);
if (!cmpPlayer)
return [];
return cmpPlayer.GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": []
};
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location.
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
// Did the start position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// Did the end position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// --------------------------------------------------------------------------------
// Do some entity cache management and check for snapping.
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// We're clearing the preview, clear the entity cache and bail.
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// Keep template data around.
}
return false;
}
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before.
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
};
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// Prevent division by zero errors further on if the start and end positions are the same.
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
// Value of 0.5 was determined through trial and error.
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// Clear the single-building preview entity (we'll be rolling our own).
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// Calculate wall placement and position preview entities.
let result = {
"pieces": [],
"cost": { "population": 0, "time": 0 }
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
// See helpers/Walls.js.
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true // Preview only, must not appear in the result.
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above.
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
// Number of entities that are required to build the entire wall, regardless of validity.
let numRequiredPieces = 0;
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// Move piece to right location.
// TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region.
// TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement.
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest.
// (TODO: Break unlikely ties by choosing the lowest entity ID.)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (data.snapToEdges)
{
let position = this.obstructionSnap.getPosition(data, template);
if (position)
return position;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySoundForPlayer = function(player, data)
{
let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player);
let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound);
if (!cmpSound)
return;
let soundGroup = cmpSound.GetSoundGroup(data.name);
if (soundGroup)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player);
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
let bucket = filtered.bucket;
if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle())
return { "idle": false };
let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable);
if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if (!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market);
return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
else if (!firstMarket)
result = { "type": "set first" };
else if (!secondMarket)
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
else
result = { "type": "set first" };
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return [];
return cmpPlayer.GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
/**
* List the GuiInterface functions that can be safely called by GUI scripts.
* (GUI scripts are non-deterministic and untrusted, so these functions must be
* appropriately careful. They are called with a first argument "player", which is
* trusted and indicates the player associated with the current client; no data should
* be returned unless this player is meant to be able to see it.)
*/
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
"GetCampaignGameEndData": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"PlaySoundForPlayer": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 25380)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 25381)
@@ -1,944 +1,975 @@
function ProductionQueue() {}
ProductionQueue.prototype.Schema =
"Allows the building to train new units and research technologies" +
"" +
"0.7" +
"" +
"\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
Resources.BuildSchema("nonNegativeDecimal", ["time"]) +
"";
ProductionQueue.prototype.ProgressInterval = 1000;
ProductionQueue.prototype.MaxQueueSize = 16;
ProductionQueue.prototype.Init = function()
{
this.nextID = 1;
this.queue = [];
// Queue items are:
// {
// "id": 1,
// "player": 1, // who paid for this batch; we need this to cope with refunds cleanly
// "unitTemplate": "units/example",
// "count": 10,
// "neededSlots": 3, // number of population slots missing for production to begin
// "resources": { "wood": 100, ... }, // resources per unit, multiply by count to get total
// "population": 1, // population per unit, multiply by count to get total
// "productionStarted": false, // true iff we have reserved population
// "entityCache": [189, ...], // The entities created but not spawned yet.
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
//
// {
// "id": 1,
// "player": 1, // who paid for this research; we need this to cope with refunds cleanly
// "technologyTemplate": "example_tech",
// "resources": { "wood": 100, ... }, // resources needed for research
// "productionStarted": false, // true iff production has started
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
};
/*
* Returns list of entities that can be trained by this building.
*/
ProductionQueue.prototype.GetEntitiesList = function()
{
return Array.from(this.entitiesMap.values());
};
/**
+ * @return {boolean} - Whether we are automatically queuing items.
+ */
+ProductionQueue.prototype.IsAutoQueueing = function()
+{
+ return !!this.autoqueuing;
+};
+
+/**
+ * Turn on Auto-Queue.
+ */
+ProductionQueue.prototype.EnableAutoQueue = function()
+{
+ this.autoqueuing = true;
+};
+
+/**
+ * Turn off Auto-Queue.
+ */
+ProductionQueue.prototype.DisableAutoQueue = function()
+{
+ delete this.autoqueuing;
+};
+
+/**
* Calculate the new list of producible entities
* and update any entities currently being produced.
*/
ProductionQueue.prototype.CalculateEntitiesMap = function()
{
// Don't reset the map, it's used below to update entities.
if (!this.entitiesMap)
this.entitiesMap = new Map();
if (!this.template.Entities)
return;
let string = this.template.Entities._string;
// Tokens can be added -> process an empty list to get them.
let addedTokens = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", "", this.entity);
if (!addedTokens && !string)
return;
addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let cmpPlayer = QueryOwnerInterface(this.entity);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
let disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
/**
* Process tokens:
* - process token modifiers (this is a bit tricky).
* - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID
* - remove disabled entities
* - upgrade templates where necessary
* This also updates currently queued production (it's more convenient to do it here).
*/
let removeAllQueuedTemplate = (token) => {
let queue = clone(this.queue);
let template = this.entitiesMap.get(token);
for (let item of queue)
if (item.unitTemplate && item.unitTemplate === template)
this.RemoveItem(item.id);
};
let updateAllQueuedTemplate = (token, updateTo) => {
let template = this.entitiesMap.get(token);
for (let item of this.queue)
if (item.unitTemplate && item.unitTemplate === template)
item.unitTemplate = updateTo;
};
let toks = string.split(/\s+/);
for (let tok of addedTokens)
toks.push(tok);
let addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
this.entitiesMap = toks.reduce((entMap, token) => {
let rawToken = token;
if (!(token in addedDict))
{
// This is a bit wasteful but I can't think of a simpler/better way.
// The list of token is unlikely to be a performance bottleneck anyways.
token = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", token, this.entity);
token = token.split(/\s+/);
if (token.every(tok => addedTokens.indexOf(tok) !== -1))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = token[0];
}
// Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
if (cmpIdentity)
token = token.replace(/\{native\}/g, cmpIdentity.GetCiv());
if (cmpPlayer)
token = token.replace(/\{civ\}/g, cmpPlayer.GetCiv());
// Filter out disabled and invalid entities.
if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = this.GetUpgradedTemplate(token);
entMap.set(rawToken, token);
updateAllQueuedTemplate(rawToken, token);
return entMap;
}, new Map());
};
/*
* Returns the upgraded template name if necessary.
*/
ProductionQueue.prototype.GetUpgradedTemplate = function(templateName)
{
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return templateName;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
while (template && template.Promotion !== undefined)
{
let requiredXp = ApplyValueModificationsToTemplate(
"Promotion/RequiredXp",
+template.Promotion.RequiredXp,
cmpPlayer.GetPlayerID(),
template);
if (requiredXp > 0)
break;
templateName = template.Promotion.Entity;
template = cmpTemplateManager.GetTemplate(templateName);
}
return templateName;
};
/*
* Returns list of technologies that can be researched by this building.
*/
ProductionQueue.prototype.GetTechnologiesList = function()
{
if (!this.template.Technologies)
return [];
let string = this.template.Technologies._string;
string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity);
if (!string)
return [];
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return [];
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return [];
let techs = string.split(/\s+/);
// Replace the civ specific technologies.
for (let i = 0; i < techs.length; ++i)
{
let tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
let civTech = tech.replace("{civ}", cmpPlayer.GetCiv());
techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic");
}
// Remove any technologies that can't be researched by this civ.
techs = techs.filter(tech =>
cmpTechnologyManager.CheckTechnologyRequirements(
DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()),
true));
let techList = [];
// Stores the tech which supersedes the key.
let superseded = {};
let disabledTechnologies = cmpPlayer.GetDisabledTechnologies();
// Add any top level technologies to an array which corresponds to the displayed icons.
// Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }.
for (let tech of techs)
{
if (disabledTechnologies && disabledTechnologies[tech])
continue;
let template = TechnologyTemplates.Get(tech);
if (!template.supersedes || techs.indexOf(template.supersedes) === -1)
techList.push(tech);
else
superseded[template.supersedes] = tech;
}
// Now make researched/in progress techs invisible.
for (let i in techList)
{
let tech = techList[i];
while (this.IsTechnologyResearchedOrInProgress(tech))
tech = superseded[tech];
techList[i] = tech;
}
let ret = [];
// This inserts the techs into the correct positions to line up the technology pairs.
for (let i = 0; i < techList.length; ++i)
{
let tech = techList[i];
if (!tech)
{
ret[i] = undefined;
continue;
}
let template = TechnologyTemplates.Get(tech);
if (template.top)
ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom };
else
ret[i] = tech;
}
return ret;
};
ProductionQueue.prototype.GetTechCostMultiplier = function()
{
let techCostMultiplier = {};
for (let res in this.template.TechCostMultiplier)
techCostMultiplier[res] = ApplyValueModificationsToEntity(
"ProductionQueue/TechCostMultiplier/" + res,
+this.template.TechCostMultiplier[res],
this.entity);
return techCostMultiplier;
};
ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech)
{
if (!tech)
return false;
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
let template = TechnologyTemplates.Get(tech);
if (template.top)
return cmpTechnologyManager.IsTechnologyResearched(template.top) ||
cmpTechnologyManager.IsInProgress(template.top) ||
cmpTechnologyManager.IsTechnologyResearched(template.bottom) ||
cmpTechnologyManager.IsInProgress(template.bottom);
return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech);
};
/*
* Adds a new batch of identical units to train or a technology to research to the production queue.
* @param {string} templateName - The template to start production on.
* @param {string} type - The type of production (i.e. "unit" or "technology").
* @param {number} count - The amount of units to be produced. Ignored for a tech.
* @param {any} metadata - Optionally any metadata to be attached to the item.
*
* @return {boolean} - Whether the addition of the item has succeeded.
*/
ProductionQueue.prototype.AddItem = function(templateName, type, count, metadata)
{
// TODO: there should be a way for the GUI to determine whether it's going
// to be possible to add a batch (based on resource costs and length limits).
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return false;
let player = cmpPlayer.GetPlayerID();
if (!this.queue.length)
{
let cmpUpgrade = Engine.QueryInterface(this.entity, IID_Upgrade);
if (cmpUpgrade && cmpUpgrade.IsUpgrading())
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("Entity is being upgraded. Cannot start production."),
"translateMessage": true
});
return false;
}
}
else if (this.queue.length >= this.MaxQueueSize)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("The production queue is full."),
"translateMessage": true,
});
return false;
}
// ToDo: Lots of duplication here, much can probably be combined,
// but requires some more refactoring.
if (type == "unit")
{
if (!Number.isInteger(count) || count <= 0)
{
error("Invalid batch count " + count);
return false;
}
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(this.GetUpgradedTemplate(templateName));
if (!template)
return false;
let costs = {};
let totalCosts = {};
for (let res in template.Cost.Resources)
{
costs[res] = ApplyValueModificationsToTemplate(
"Cost/Resources/" + res,
+template.Cost.Resources[res],
player,
template);
totalCosts[res] = Math.floor(count * costs[res]);
}
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer.TrySubtractResources(totalCosts))
return false;
if (template.TrainingRestrictions)
{
let unitCategory = template.TrainingRestrictions.Category;
let cmpPlayerEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
{
cmpPlayerEntityLimits.ChangeCount(unitCategory, count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(templateName, count);
}
}
let buildTime = ApplyValueModificationsToTemplate(
"Cost/BuildTime",
+template.Cost.BuildTime,
player,
template);
let time = this.GetBatchTime(count) * buildTime * 1000;
this.queue.push({
"id": this.nextID++,
"player": player,
"unitTemplate": templateName,
"count": count,
"metadata": metadata,
"resources": costs,
"population": ApplyValueModificationsToTemplate(
"Cost/Population",
+template.Cost.Population,
player,
template),
"productionStarted": false,
"timeTotal": time,
"timeRemaining": time
});
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("OnTrainingQueued", {
"playerid": player,
"unitTemplate": templateName,
"count": count,
"metadata": metadata,
"trainerEntity": this.entity
});
}
else if (type == "technology")
{
if (!TechnologyTemplates.Has(templateName))
return false;
if (!this.GetTechnologiesList().some(tech =>
tech &&
(tech == templateName ||
tech.pair &&
(tech.top == templateName || tech.bottom == templateName))))
{
error("This entity cannot research " + templateName);
return false;
}
let template = TechnologyTemplates.Get(templateName);
let techCostMultiplier = this.GetTechCostMultiplier();
let cost = {};
if (template.cost)
for (let res in template.cost)
cost[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]);
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer.TrySubtractResources(cost))
return false;
// Tell the technology manager that we have started researching this
// such that players can't research the same thing twice.
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.QueuedResearch(templateName, this.entity);
let time = techCostMultiplier.time * (template.researchTime || 0) * 1000;
this.queue.push({
"id": this.nextID++,
"player": player,
"count": 1,
"technologyTemplate": templateName,
"resources": cost,
"productionStarted": false,
"timeTotal": time,
"timeRemaining": time
});
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("OnResearchQueued", {
"playerid": player,
"technologyTemplate": templateName,
"researcherEntity": this.entity
});
}
else
{
warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue");
return false;
}
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
if (!this.timer)
this.StartTimer();
return true;
};
/*
* Removes an item from the queue.
* Refunds resource costs and population reservations.
* item.player is used as this.entity's owner may have changed.
*/
ProductionQueue.prototype.RemoveItem = function(id)
{
let itemIndex = this.queue.findIndex(item => item.id == id);
if (itemIndex == -1)
return;
let item = this.queue[itemIndex];
// Destroy any cached entities (those which didn't spawn for some reason).
if (item.entityCache && item.entityCache.length)
{
for (let ent of item.entityCache)
Engine.DestroyEntity(ent);
item.entityCache = [];
}
if (item.unitTemplate)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(item.unitTemplate);
if (template.TrainingRestrictions)
{
let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -item.count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(item.unitTemplate, -item.count);
}
}
let totalCosts = {};
let cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
for (let resource in item.resources)
{
totalCosts[resource] = Math.floor(item.count * item.resources[resource]);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]);
}
let cmpPlayer = QueryPlayerIDInterface(item.player);
if (cmpPlayer)
{
cmpPlayer.AddResources(totalCosts);
if (item.productionStarted && item.unitTemplate)
cmpPlayer.UnReservePopulationSlots(item.population * item.count);
if (itemIndex == 0)
cmpPlayer.UnBlockTraining();
}
if (item.technologyTemplate)
{
let cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.StoppedResearch(item.technologyTemplate, true);
}
this.queue.splice(itemIndex, 1);
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
if (!this.queue.length)
this.StopTimer();
};
ProductionQueue.prototype.SetAnimation = function(name)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation(name, false, 1);
};
/*
* Returns basic data from all batches in the production queue.
*/
ProductionQueue.prototype.GetQueue = function()
{
return this.queue.map(item => ({
"id": item.id,
"unitTemplate": item.unitTemplate,
"technologyTemplate": item.technologyTemplate,
"count": item.count,
"neededSlots": item.neededSlots,
"progress": 1 - (item.timeRemaining / (item.timeTotal || 1)),
"timeRemaining": item.timeRemaining,
"metadata": item.metadata
}));
};
/*
* Removes all existing batches from the queue.
*/
ProductionQueue.prototype.ResetQueue = function()
{
while (this.queue.length)
this.RemoveItem(this.queue[0].id);
};
/*
* Returns batch build time.
*/
ProductionQueue.prototype.GetBatchTime = function(batchSize)
{
// TODO: work out what equation we should use here.
return Math.pow(batchSize, ApplyValueModificationsToEntity(
"ProductionQueue/BatchTimeModifier",
+this.template.BatchTimeModifier,
this.entity));
};
ProductionQueue.prototype.OnOwnershipChanged = function(msg)
{
// Reset the production queue whenever the owner changes.
// (This should prevent players getting surprised when they capture
// an enemy building, and then loads of the enemy's civ's soldiers get
// created from it. Also it means we don't have to worry about
// updating the reserved pop slots.)
this.ResetQueue();
if (msg.to != INVALID_PLAYER)
this.CalculateEntitiesMap();
};
ProductionQueue.prototype.OnCivChanged = function()
{
this.CalculateEntitiesMap();
};
/*
* This function creates the entities and places them in world if possible
* (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
* @param {Object} item - The item to spawn units for.
* @param {number} item.count - The number of entities to spawn.
* @param {string} item.player - The owner of the item.
* @param {string} item.unitTemplate - The template to spawn.
* @param {any} - item.metadata - Optionally any metadata to add to the TrainingFinished message.
*
* @return {number} - The number of successfully created entities
*/
ProductionQueue.prototype.SpawnUnits = function(item)
{
let createdEnts = [];
let spawnedEnts = [];
// We need entities to test spawning, but we don't want to waste resources,
// so only create them once and use as needed.
if (!item.entityCache)
{
item.entityCache = [];
for (let i = 0; i < item.count; ++i)
item.entityCache.push(Engine.AddEntity(item.unitTemplate));
}
let autoGarrison;
let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
if (cmpRallyPoint)
{
let data = cmpRallyPoint.GetData()[0];
if (data && data.target && data.target == this.entity && data.command == "garrison")
autoGarrison = true;
}
let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let positionSelf = cmpPosition && cmpPosition.GetPosition();
let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
while (item.entityCache.length)
{
let ent = item.entityCache[0];
let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
let garrisoned = false;
if (autoGarrison)
{
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
{
// Temporary owner affectation needed for GarrisonHolder checks.
cmpNewOwnership.SetOwnerQuiet(item.player);
garrisoned = cmpGarrisonable.Garrison(this.entity);
cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
}
}
if (!garrisoned)
{
let pos = cmpFootprint.PickSpawnPoint(ent);
if (pos.y < 0)
break;
let cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
cmpNewPosition.JumpTo(pos.x, pos.z);
if (positionSelf)
cmpNewPosition.SetYRotation(positionSelf.horizAngleTo(pos));
spawnedEnts.push(ent);
}
// Decrement entity count in the EntityLimits component
// since it will be increased by EntityLimits.OnGlobalOwnershipChanged,
// i.e. we replace a 'trained' entity by 'alive' one.
// Must be done after spawn check so EntityLimits decrements only if unit spawns.
if (cmpPlayerEntityLimits)
{
let cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
if (cmpTrainingRestrictions)
cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1);
}
cmpNewOwnership.SetOwner(item.player);
if (cmpPlayerStatisticsTracker)
cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
item.entityCache.shift();
createdEnts.push(ent);
}
if (spawnedEnts.length && !autoGarrison && cmpRallyPoint)
for (let com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts))
ProcessCommand(item.player, com);
if (createdEnts.length)
{
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects).
PlaySound("trained", createdEnts[0]);
Engine.PostMessage(this.entity, MT_TrainingFinished, {
"entities": createdEnts,
"owner": item.player,
"metadata": item.metadata
});
}
return createdEnts.length;
};
/*
* Increments progress on the first item in the production queue and blocks the
* queue if population limit is reached or some units failed to spawn.
* @param {Object} data - Unused in this case.
* @param {number} lateness - The time passed since the expected time to fire the function.
*/
ProductionQueue.prototype.ProgressTimeout = function(data, lateness)
{
if (this.paused)
return;
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
// Allocate available time to as many queue items as it takes
// until we've used up all the time (so that we work accurately
// with items that take fractions of a second).
let time = this.ProgressInterval + lateness;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
while (this.queue.length)
{
let item = this.queue[0];
if (!item.productionStarted)
{
if (item.unitTemplate)
{
let template = cmpTemplateManager.GetTemplate(item.unitTemplate);
item.population = ApplyValueModificationsToTemplate(
"Cost/Population",
+template.Cost.Population,
item.player,
template);
item.neededSlots = cmpPlayer.TryReservePopulationSlots(item.population * item.count);
if (item.neededSlots)
{
cmpPlayer.BlockTraining();
return;
}
this.SetAnimation("training");
cmpPlayer.UnBlockTraining();
+
+ // AutoQueue: We add the second batch after starting the first.
+ // (As opposed to when the first batch finishes.)
+ // This is to make the feature not infinitely better than good micro.
+ if (this.autoqueuing)
+ this.AddItem(item.unitTemplate, "unit", item.count, item.metadata);
+
Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity });
}
if (item.technologyTemplate)
{
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.StartedResearch(item.technologyTemplate, true);
else
warn("Failed to start researching " + item.technologyTemplate + ": No TechnologyManager available.");
this.SetAnimation("researching");
}
item.productionStarted = true;
}
if (item.timeRemaining > time)
{
item.timeRemaining -= time;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
return;
}
if (item.unitTemplate)
{
let numSpawned = this.SpawnUnits(item);
if (numSpawned)
cmpPlayer.UnReservePopulationSlots(item.population * numSpawned);
if (numSpawned == item.count)
{
cmpPlayer.UnBlockTraining();
delete this.spawnNotified;
}
else
{
if (numSpawned)
{
item.count -= numSpawned;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
}
cmpPlayer.BlockTraining();
if (!this.spawnNotified)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Can't find free space to spawn trained units"),
"translateMessage": true
});
this.spawnNotified = true;
}
return;
}
}
if (item.technologyTemplate)
{
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.ResearchTechnology(item.technologyTemplate);
else
warn("Failed to stop researching " + item.technologyTemplate + ": No TechnologyManager available.");
let template = TechnologyTemplates.Get(item.technologyTemplate);
if (template && template.soundComplete)
{
let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager)
cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity);
}
}
time -= item.timeRemaining;
this.queue.shift();
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
}
if (!this.queue.length)
this.StopTimer();
};
ProductionQueue.prototype.PauseProduction = function()
{
this.StopTimer();
this.paused = true;
};
ProductionQueue.prototype.UnpauseProduction = function()
{
delete this.paused;
this.StartTimer();
};
ProductionQueue.prototype.StartTimer = function()
{
if (this.timer)
return;
this.timer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetInterval(
this.entity,
IID_ProductionQueue,
"ProgressTimeout",
this.ProgressInterval,
this.ProgressInterval,
null
);
};
ProductionQueue.prototype.StopTimer = function()
{
if (!this.timer)
return;
this.SetAnimation("idle");
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.timer);
delete this.timer;
};
ProductionQueue.prototype.OnValueModification = function(msg)
{
// If the promotion requirements of units is changed,
// update the entities list so that automatically promoted units are shown
// appropriately in the list.
if (msg.component != "Promotion" && (msg.component != "ProductionQueue" ||
!msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/"))))
return;
if (msg.entities.indexOf(this.entity) === -1)
return;
// This also updates the queued production if necessary.
this.CalculateEntitiesMap();
// Inform the GUI that it'll need to recompute the selection panel.
// TODO: it would be better to only send the message if something actually changing
// for the current production queue.
let cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
};
ProductionQueue.prototype.HasQueuedProduction = function()
{
return this.queue.length > 0;
};
ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.CalculateEntitiesMap();
};
ProductionQueue.prototype.OnGarrisonedStateChanged = function(msg)
{
if (msg.holderID != INVALID_ENTITY)
this.PauseProduction();
else
this.UnpauseProduction();
};
Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 25380)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 25381)
@@ -1,552 +1,625 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/BuildRestrictions.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/TrainingRestrictions.js");
Engine.LoadComponentScript("interfaces/Trigger.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("EntityLimits.js");
Engine.LoadComponentScript("Timer.js");
Engine.RegisterGlobal("Resources", {
"BuildSchema": (a, b) => {}
});
Engine.LoadComponentScript("ProductionQueue.js");
Engine.LoadComponentScript("TrainingRestrictions.js");
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value);
function testEntitiesList()
{
Engine.RegisterGlobal("TechnologyTemplates", {
"Has": name => name == "phase_town_athen" || name == "phase_city_athen",
"Get": () => ({})
});
const productionQueueId = 6;
const playerId = 1;
const playerEntityID = 2;
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({})
});
let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", {
"Entities": { "_string": "units/{civ}/cavalry_javelineer_b " +
"units/{civ}/infantry_swordsman_b " +
"units/{native}/support_female_citizen" },
"Technologies": { "_string": "gather_fishing_net " +
"phase_town_{civ} " +
"phase_city_{civ}" }
});
cmpProductionQueue.GetUpgradedTemplate = (template) => template;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEntityID
});
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetDisabledTemplates": () => ({}),
"GetPlayerID": () => playerId
});
AddMock(playerEntityID, IID_TechnologyManager, {
"CheckTechnologyRequirements": () => true,
"IsInProgress": () => false,
"IsTechnologyResearched": () => false
});
AddMock(productionQueueId, IID_Ownership, {
"GetOwner": () => playerId
});
AddMock(productionQueueId, IID_Identity, {
"GetCiv": () => "iber"
});
AddMock(productionQueueId, IID_Upgrade, {
"IsUpgrading": () => false
});
cmpProductionQueue.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
);
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetTechnologiesList(),
["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
);
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": name => name == "units/iber/support_female_citizen",
"GetTemplate": name => ({})
});
cmpProductionQueue.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber/support_female_citizen"]);
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({})
});
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }),
"GetPlayerID": () => playerId
});
cmpProductionQueue.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": true }),
"GetPlayerID": () => playerId
});
cmpProductionQueue.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/iber/cavalry_javelineer_b", "units/iber/support_female_citizen"]
);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "athen",
"GetDisabledTechnologies": () => ({ "gather_fishing_net": true }),
"GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }),
"GetPlayerID": () => playerId
});
cmpProductionQueue.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/athen/cavalry_javelineer_b", "units/iber/support_female_citizen"]
);
TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), ["phase_town_athen",
"phase_city_athen"]
);
AddMock(playerEntityID, IID_TechnologyManager, {
"CheckTechnologyRequirements": () => true,
"IsInProgress": () => false,
"IsTechnologyResearched": tech => tech == "phase_town_athen"
});
TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), [undefined, "phase_city_athen"]);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetPlayerID": () => playerId
});
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetTechnologiesList(),
["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
);
}
function regression_test_d1879()
{
// Setup
let playerEnt = 2;
let playerID = 1;
let testEntity = 3;
let spawedEntityIDs = [4, 5, 6, 7, 8];
let spawned = 0;
Engine.AddEntity = () => {
let id = spawedEntityIDs[spawned++];
ConstructComponent(id, "TrainingRestrictions", {
"Category": "some_limit"
});
AddMock(id, IID_Identity, {
"GetClassesList": () => []
});
AddMock(id, IID_Position, {
"JumpTo": () => {}
});
AddMock(id, IID_Ownership, {
"SetOwner": (pid) => {
let cmpEntLimits = QueryOwnerInterface(id, IID_EntityLimits);
cmpEntLimits.OnGlobalOwnershipChanged({
"entity": id,
"from": -1,
"to": pid
});
},
"GetOwner": () => playerID
});
return id;
};
ConstructComponent(playerEnt, "EntityLimits", {
"Limits": {
"some_limit": 8
},
"LimitChangers": {},
"LimitRemovers": {}
});
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"PushNotification": () => {}
});
AddMock(SYSTEM_ENTITY, IID_Trigger, {
"CallEvent": () => {}
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": (ent, iid, func) => 1,
"CancelTimer": (id) => {}
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({
"Cost": {
"BuildTime": 0,
"Population": 1,
"Resources": {}
},
"TrainingRestrictions": {
"Category": "some_limit",
"MatchLimit": "7"
}
})
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEnt
});
AddMock(playerEnt, IID_Player, {
"GetCiv": () => "iber",
"GetPlayerID": () => playerID,
"GetTimeMultiplier": () => 0,
"BlockTraining": () => {},
"UnBlockTraining": () => {},
"UnReservePopulationSlots": () => {},
"TrySubtractResources": () => true,
"AddResources": () => true,
"TryReservePopulationSlots": () => false // Always have pop space.
});
AddMock(testEntity, IID_Ownership, {
"GetOwner": () => playerID
});
let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
"Entities": { "_string": "some_template" },
"BatchTimeModifier": 1
});
let cmpEntLimits = QueryOwnerInterface(testEntity, IID_EntityLimits);
TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 8));
TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 9));
TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5, "some_template", 8));
TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 10, "some_template", 8));
// Check that the entity limits do get updated if the spawn succeeds.
AddMock(testEntity, IID_Footprint, {
"PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 })
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
// Now check that it doesn't get updated when the spawn doesn't succeed.
AddMock(testEntity, IID_Footprint, {
"PickSpawnPoint": () => ({ "x": -1, "y": -1, "z": -1 })
});
AddMock(testEntity, IID_Upgrade, {
"IsUpgrading": () => false
});
cmpProdQueue.AddItem("some_template", "unit", 3);
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 6);
// Check that when the batch is removed the counts are subtracted again.
cmpProdQueue.RemoveItem(cmpProdQueue.GetQueue()[0].id);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
}
function test_batch_adding()
{
let playerEnt = 2;
let playerID = 1;
let testEntity = 3;
ConstructComponent(playerEnt, "EntityLimits", {
"Limits": {
"some_limit": 8
},
"LimitChangers": {},
"LimitRemovers": {}
});
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"PushNotification": () => {}
});
AddMock(SYSTEM_ENTITY, IID_Trigger, {
"CallEvent": () => {}
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": (ent, iid, func) => 1
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({
"Cost": {
"BuildTime": 0,
"Population": 1,
"Resources": {}
},
"TrainingRestrictions": {
"Category": "some_limit"
}
})
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEnt
});
AddMock(playerEnt, IID_Player, {
"GetCiv": () => "iber",
"GetPlayerID": () => playerID,
"GetTimeMultiplier": () => 0,
"BlockTraining": () => {},
"UnBlockTraining": () => {},
"UnReservePopulationSlots": () => {},
"TrySubtractResources": () => true,
"TryReservePopulationSlots": () => false // Always have pop space.
});
AddMock(testEntity, IID_Ownership, {
"GetOwner": () => playerID
});
let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
"Entities": { "_string": "some_template" },
"BatchTimeModifier": 1
});
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
AddMock(testEntity, IID_Upgrade, {
"IsUpgrading": () => true
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
AddMock(testEntity, IID_Upgrade, {
"IsUpgrading": () => false
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
}
function test_batch_removal()
{
let playerEnt = 2;
let playerID = 1;
let testEntity = 3;
ConstructComponent(playerEnt, "EntityLimits", {
"Limits": {
"some_limit": 8
},
"LimitChangers": {},
"LimitRemovers": {}
});
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"PushNotification": () => {}
});
AddMock(SYSTEM_ENTITY, IID_Trigger, {
"CallEvent": () => {}
});
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", null);
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({
"Cost": {
"BuildTime": 0,
"Population": 1,
"Resources": {}
},
"TrainingRestrictions": {
"Category": "some_limit"
}
})
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEnt
});
let cmpPlayer = AddMock(playerEnt, IID_Player, {
"GetCiv": () => "iber",
"GetPlayerID": () => playerID,
"GetTimeMultiplier": () => 0,
"BlockTraining": () => {},
"UnBlockTraining": () => {},
"UnReservePopulationSlots": () => {},
"TrySubtractResources": () => true,
"AddResources": () => {},
"TryReservePopulationSlots": () => 1
});
let cmpPlayerBlockSpy = new Spy(cmpPlayer, "BlockTraining");
let cmpPlayerUnblockSpy = new Spy(cmpPlayer, "UnBlockTraining");
AddMock(testEntity, IID_Ownership, {
"GetOwner": () => playerID
});
let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
"Entities": { "_string": "some_template" },
"BatchTimeModifier": 1
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpPlayerBlockSpy._called, 1);
cmpProdQueue.AddItem("some_template", "unit", 2);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
cmpProdQueue.RemoveItem(1);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 1);
cmpProdQueue.RemoveItem(2);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 2);
cmpProdQueue.AddItem("some_template", "unit", 3);
cmpProdQueue.AddItem("some_template", "unit", 3);
cmpPlayer.TryReservePopulationSlots = () => false;
cmpProdQueue.RemoveItem(3);
TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 3);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 4);
}
function test_token_changes()
{
const ent = 10;
let cmpProductionQueue = ConstructComponent(10, "ProductionQueue", {
"Entities": { "_string": "units/{civ}/a " +
"units/{civ}/b" },
"Technologies": { "_string": "a " +
"b_{civ} " +
"c_{civ}" },
"BatchTimeModifier": 1
});
cmpProductionQueue.GetUpgradedTemplate = (template) => template;
// Merges interface of multiple components because it's enough here.
Engine.RegisterGlobal("QueryOwnerInterface", () => ({
// player
"GetCiv": () => "test",
"GetDisabledTemplates": () => [],
"GetDisabledTechnologies": () => [],
"TryReservePopulationSlots": () => false, // Always have pop space.
"TrySubtractResources": () => true,
"UnBlockTraining": () => {},
"AddResources": () => {},
"GetPlayerID": () => 1,
// entitylimits
"ChangeCount": () => {},
// techmanager
"CheckTechnologyRequirements": () => true,
"IsTechnologyResearched": () => false,
"IsInProgress": () => false
}));
Engine.RegisterGlobal("QueryPlayerIDInterface", QueryOwnerInterface);
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"SetSelectionDirty": () => {}
});
// Test Setup
cmpProductionQueue.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(), ["units/test/a", "units/test/b"]
);
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetTechnologiesList(), ["a", "b_generic", "c_generic"]
);
// Add a unit of each type to our queue, validate.
cmpProductionQueue.AddItem("units/test/a", "unit", 1, {});
cmpProductionQueue.AddItem("units/test/b", "unit", 1, {});
TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test/a");
TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[1].unitTemplate, "units/test/b");
// Add a modifier that replaces unit A with unit C,
// adds a unit D and removes unit B from the roster.
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, val) => {
return HandleTokens(val, "units/{civ}/a>units/{civ}/c units/{civ}/d -units/{civ}/b");
});
cmpProductionQueue.OnValueModification({
"component": "ProductionQueue",
"valueNames": ["ProductionQueue/Entities/_string"],
"entities": [ent]
});
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(), ["units/test/c", "units/test/d"]
);
TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test/c");
TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue().length, 1);
}
+function test_auto_queue()
+{
+ let playerEnt = 2;
+ let playerID = 1;
+ let testEntity = 3;
+
+ ConstructComponent(playerEnt, "EntityLimits", {
+ "Limits": {
+ "some_limit": 8
+ },
+ "LimitChangers": {},
+ "LimitRemovers": {}
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
+ "PushNotification": () => {}
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_Trigger, {
+ "CallEvent": () => {}
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_Timer, {
+ "SetInterval": (ent, iid, func) => 1
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
+ "TemplateExists": () => true,
+ "GetTemplate": name => ({
+ "Cost": {
+ "BuildTime": 0,
+ "Population": 1,
+ "Resources": {}
+ },
+ "TrainingRestrictions": {
+ "Category": "some_limit"
+ }
+ })
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
+ "GetPlayerByID": id => playerEnt
+ });
+
+ AddMock(playerEnt, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetPlayerID": () => playerID,
+ "GetTimeMultiplier": () => 0,
+ "BlockTraining": () => {},
+ "UnBlockTraining": () => {},
+ "UnReservePopulationSlots": () => {},
+ "TrySubtractResources": () => true,
+ "TryReservePopulationSlots": () => false // Always have pop space.
+ });
+
+ AddMock(testEntity, IID_Ownership, {
+ "GetOwner": () => playerID
+ });
+
+ let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
+ "Entities": { "_string": "some_template" },
+ "BatchTimeModifier": 1
+ });
+
+ cmpProdQueue.EnableAutoQueue();
+
+ cmpProdQueue.AddItem("some_template", "unit", 3);
+ TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
+ cmpProdQueue.ProgressTimeout(null, 0);
+ TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
+}
+
testEntitiesList();
regression_test_d1879();
test_batch_adding();
test_batch_removal();
+test_auto_queue();
test_token_changes();
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25380)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25381)
@@ -1,1815 +1,1836 @@
// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
function ProcessCommand(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return;
let data = {
"cmpPlayer": cmpPlayer,
"controlAllUnits": cmpPlayer.CanControlAllUnits()
};
if (cmd.entities)
data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
// TODO: queuing order and forcing formations doesn't really work.
// To play nice, we'll still no-formation queued order if units are in formation
// but the opposite perhaps ought to be implemented.
if (!cmd.queued || cmd.formation == NULL_FORMATION)
data.formation = cmd.formation || undefined;
// Allow focusing the camera on recent commands
let commandData = {
"type": "playercommand",
"players": [player],
"cmd": cmd
};
// Save the position, since the GUI event is received after the unit died
if (cmd.type == "delete-entities")
{
let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position);
commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D();
}
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification(commandData);
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// Now handle various commands
if (g_Commands[cmd.type])
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("OnPlayerCommand", { "player": player, "cmd": cmd });
g_Commands[cmd.type](player, cmd, data);
}
else
error("Invalid command: unknown command type: "+uneval(cmd));
}
var g_Commands = {
"aichat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
var notification = { "players": [player] };
for (var key in cmd)
notification[key] = cmd[key];
cmpGuiInterface.PushNotification(notification);
},
"cheat": function(player, cmd, data)
{
Cheat(cmd);
},
"collect-treasure": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CollectTreasure(cmd.target, cmd.autocontinue, cmd.queued);
});
},
"collect-treasure-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, cmd.autocontinue, cmd.queued);
});
},
"diplomacy": function(player, cmd, data)
{
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (data.cmpPlayer.GetLockTeams() ||
cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive())
return;
switch(cmd.to)
{
case "ally":
data.cmpPlayer.SetAlly(cmd.player);
break;
case "neutral":
data.cmpPlayer.SetNeutral(cmd.player);
break;
case "enemy":
data.cmpPlayer.SetEnemy(cmd.player);
break;
default:
warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "diplomacy",
"players": [player],
"targetPlayer": cmd.player,
"status": cmd.to
});
},
"tribute": function(player, cmd, data)
{
data.cmpPlayer.TributeResource(cmd.player, cmd.amounts);
},
"control-all": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - control all units)")
});
data.cmpPlayer.SetControlAllUnits(cmd.flag);
},
"reveal-map": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - reveal map)")
});
// Reveal the map for all players, not just the current player,
// primarily to make it obvious to everyone that the player is cheating
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
},
"walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront);
});
},
"walk-custom": function(player, cmd, data)
{
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued, cmd.pushFront);
});
},
"walk-to-range": function(player, cmd, data)
{
// Only used by the AI
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued, cmd.pushFront);
}
},
"attack-walk": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront);
});
},
"attack-walk-custom": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront);
});
},
"attack": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
if (g_DebugCommands && !allowCapture &&
!(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued, cmd.pushFront);
});
},
"patrol": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI =>
cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued)
);
},
"heal": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Heal(cmd.target, cmd.queued, cmd.pushFront);
});
},
"repair": function(player, cmd, data)
{
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued, cmd.pushFront);
});
},
"gather": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Gather(cmd.target, cmd.queued, cmd.pushFront);
});
},
"gather-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued, cmd.pushFront);
});
},
"returnresource": function(player, cmd, data)
{
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued, cmd.pushFront);
});
},
"back-to-work": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(!cmpUnitAI || !cmpUnitAI.BackToWork())
notifyBackToWorkFailure(player);
}
},
"remove-guard": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.RemoveGuard();
}
},
"train": function(player, cmd, data)
{
if (!Number.isInteger(cmd.count) || cmd.count <= 0)
{
warn("Invalid command: can't train " + uneval(cmd.count) + " units");
return;
}
// Check entity limits
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
var unitCategory = null;
if (template.TrainingRestrictions)
unitCategory = template.TrainingRestrictions.Category;
// Verify that the building(s) can be controlled by the player
if (data.entities.length <= 0)
{
if (g_DebugCommands)
warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
for (let ent of data.entities)
{
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));
continue;
}
}
var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
continue;
}
var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
// Check if the building can train the unit
// TODO: the AI API does not take promotion technologies into account for the list
// of trainable units (taken directly from the unit template). Here is a temporary fix.
if (queue && data.cmpPlayer.IsAI())
{
var list = queue.GetEntitiesList();
if (list.indexOf(cmd.template) === -1 && cmd.promoted)
{
for (var promoted of cmd.promoted)
{
if (list.indexOf(promoted) === -1)
continue;
cmd.template = promoted;
break;
}
}
}
if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
if ("metadata" in cmd)
queue.AddItem(cmd.template, "unit", +cmd.count, cmd.metadata);
else
queue.AddItem(cmd.template, "unit", +cmd.count);
}
},
"research": function(player, cmd, data)
{
var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.AddItem(cmd.template, "technology");
},
"stop-production": function(player, cmd, data)
{
let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.RemoveItem(cmd.id);
},
"construct": function(player, cmd, data)
{
TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"construct-wall": function(player, cmd, data)
{
TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"delete-entities": function(player, cmd, data)
{
for (let ent of data.entities)
{
if (!data.controlAllUnits)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity && cmpIdentity.IsUndeletable())
continue;
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable &&
cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2)
continue;
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather())
continue;
}
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
{
let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health);
if (cmpMiragedHealth)
cmpMiragedHealth.Kill();
else
Engine.DestroyEntity(cmpMirage.parent);
Engine.DestroyEntity(ent);
continue;
}
let cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
else
Engine.DestroyEntity(ent);
}
},
"set-rallypoint": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
if (!cmd.queued)
cmpRallyPoint.Unset();
cmpRallyPoint.AddPosition(cmd.x, cmd.z);
cmpRallyPoint.AddData(clone(cmd.data));
}
}
},
"unset-rallypoint": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Reset();
}
},
"resign": function(player, cmd, data)
{
data.cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned."));
},
"occupy-turret": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.OccupyTurret(cmd.target, cmd.queued);
});
},
"garrison": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Garrison(cmd.target, cmd.queued, cmd.pushFront);
});
},
"guard": function(player, cmd, data)
{
if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Guard(cmd.target, cmd.queued, cmd.pushFront);
});
},
"stop": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Stop(cmd.queued);
});
},
"leave-turret": function(player, cmd, data)
{
let notUnloaded = 0;
for (let ent of data.entities)
{
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
++notUnloaded;
}
if (notUnloaded)
notifyUnloadFailure(player);
},
"unload-turrets": function(player, cmd, data)
{
let notUnloaded = 0;
for (let ent of data.entities)
{
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
for (let turret of cmpTurretHolder.GetEntities())
{
let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
++notUnloaded;
}
}
if (notUnloaded)
notifyUnloadFailure(player);
},
"unload": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
var notUngarrisoned = 0;
// The owner can ungarrison every garrisoned unit
if (IsOwnedByPlayer(player, cmd.garrisonHolder))
data.entities = cmd.entities;
for (let ent of data.entities)
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
++notUngarrisoned;
if (notUngarrisoned != 0)
notifyUnloadFailure(player, cmd.garrisonHolder);
},
"unload-template": function(player, cmd, data)
{
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
// Only the owner of the garrisonHolder may unload entities from any owners
if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits
&& player != +cmd.owner)
continue;
if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all))
notifyUnloadFailure(player, garrisonHolder);
}
}
},
"unload-all-by-owner": function(player, cmd, data)
{
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player))
notifyUnloadFailure(player, garrisonHolder);
}
},
"unload-all": function(player, cmd, data)
{
var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
notifyUnloadFailure(player, garrisonHolder);
}
},
"alert-raise": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.RaiseAlert();
}
},
"alert-end": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.EndOfAlert();
}
},
"formation": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => {
cmpUnitAI.MoveIntoFormation(cmd);
});
},
"promote": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - promoted units)"),
"translateMessage": true
});
for (let ent of cmd.entities)
{
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
}
},
"stance": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && !cmpUnitAI.IsTurret())
cmpUnitAI.SwitchToStance(cmd.name);
}
},
"lock-gate": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (!cmpGate)
continue;
if (cmd.lock)
cmpGate.LockGate();
else
cmpGate.UnlockGate();
}
},
"setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
});
},
"cancel-setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CancelSetupTradeRoute(cmd.target);
});
},
"set-trading-goods": function(player, cmd, data)
{
data.cmpPlayer.SetTradingGoods(cmd.tradingGoods);
},
"barter": function(player, cmd, data)
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
cmpBarter.ExchangeResources(player, cmd.sell, cmd.buy, cmd.amount);
},
"set-shading-color": function(player, cmd, data)
{
// Prevent multiplayer abuse
if (!data.cmpPlayer.IsAI())
return;
// Debug command to make an entity brightly colored
for (let ent of cmd.entities)
{
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0
}
},
"pack": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
if (cmd.pack)
cmpUnitAI.Pack(cmd.queued, cmd.pushFront);
else
cmpUnitAI.Unpack(cmd.queued, cmd.pushFront);
}
},
"cancel-pack": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
if (cmd.pack)
cmpUnitAI.CancelPack(cmd.queued, cmd.pushFront);
else
cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront);
}
},
"upgrade": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template))
continue;
if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template))
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.")
});
continue;
}
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd));
continue;
}
let cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
let requiredTechnology = cmpUpgrade.GetRequiredTechnology(cmd.template);
if (requiredTechnology && (!cmpTechnologyManager || !cmpTechnologyManager.IsTechnologyResearched(requiredTechnology)))
{
if (g_DebugCommands)
warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd));
continue;
}
cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer);
}
},
"cancel-upgrade": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
cmpUpgrade.CancelUpgrade(player);
}
},
"attack-request": function(player, cmd, data)
{
// Send a chat message to human players
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": "/allies " + markForTranslation("Attack against %(_player_)s requested."),
"translateParameters": ["_player_"],
"parameters": { "_player_": cmd.player }
});
// And send an attackRequest event to the AIs
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("AttackRequest", cmd);
},
"spy-request": function(player, cmd, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => {
let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing);
return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player);
}));
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "spy-response",
"players": [player],
"target": cmd.player,
"entity": ent
});
if (ent)
Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source);
else
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy");
IncurBribeCost(template, player, cmd.player, true);
// update statistics for failed bribes
let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
if (cmpBribesStatisticsTracker)
cmpBribesStatisticsTracker.IncreaseFailedBribesCounter();
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("There are no bribable units"),
"translateMessage": true
});
}
},
"diplomacy-request": function(player, cmd, data)
{
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("DiplomacyRequest", cmd);
},
"tribute-request": function(player, cmd, data)
{
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("TributeRequest", cmd);
},
"dialog-answer": function(player, cmd, data)
{
// Currently nothing. Triggers can read it anyway, and send this
// message to any component you like.
},
"set-dropsite-sharing": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite && cmpResourceDropsite.IsSharable())
cmpResourceDropsite.SetSharing(cmd.shared);
}
},
+
+ "autoqueue-on": function(player, cmd, data)
+ {
+ for (let ent of data.entities)
+ {
+ let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
+ if (cmpProductionQueue)
+ cmpProductionQueue.EnableAutoQueue();
+ }
+ },
+
+ "autoqueue-off": function(player, cmd, data)
+ {
+ for (let ent of data.entities)
+ {
+ let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
+ if (cmpProductionQueue)
+ cmpProductionQueue.DisableAutoQueue();
+ }
+ },
+
};
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Unable to unload unit(s)."),
"translateMessage": true
});
}
/**
* Sends a GUI notification about worker(s) that failed to go back to work.
*/
function notifyBackToWorkFailure(player)
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Some unit(s) can't go back to work"),
"translateMessage": true
});
}
/**
* Sends a GUI notification about entities that can't be controlled.
* @param {number} player - The player-ID of the player that needs to receive this message.
*/
function notifyOrderFailure(entity, player)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
if (!cmpIdentity)
return;
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": sprintf(markForTranslation("%(unit)s can't be controlled."), {
"unit": cmpIdentity.GetGenericName()
}),
"translateMessage": true
});
}
/**
* Get some information about the formations used by entities.
*/
function ExtractFormations(ents)
{
let entities = []; // Entities with UnitAI.
let members = {}; // { formationentity: [ent, ent, ...], ... }
let templates = {}; // { formationentity: template }
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
entities.push(ent);
let fid = cmpUnitAI.GetFormationController();
if (fid == INVALID_ENTITY)
continue;
if (!members[fid])
{
members[fid] = [];
templates[fid] = cmpUnitAI.GetFormationTemplate();
}
members[fid].push(ent);
}
return {
"entities": entities,
"members": members,
"templates": templates
};
}
/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
function GetDockAngle(template, x, z)
{
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
return undefined;
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
else if (template.Footprint.Circle)
halfSize = template.Footprint.Circle["@radius"];
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = x - d*Math.sin(angle);
var nz = z + d*Math.cos(angle);
if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
waterPoints.push(i);
}
var consec = [];
var length = waterPoints.length;
if (!length)
continue;
for (var i = 0; i < length; ++i)
{
var count = 0;
for (let j = 0; j < length - 1; ++j)
{
if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length])
++count;
else
break;
}
consec[i] = count;
}
var start = 0;
var count = 0;
for (var c in consec)
{
if (consec[c] > count)
{
start = c;
count = consec[c];
}
}
// If we've found a shoreline, stop searching
if (count != numPoints-1)
return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI;
}
return undefined;
}
/**
* Attempts to construct a building using the specified parameters.
* Returns true on success, false on failure.
*/
function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
{
// Message structure:
// {
// "type": "construct",
// "entities": [...], // entities that will be ordered to construct the building (if applicable)
// "template": "...", // template name of the entity being constructed
// "x": ...,
// "z": ...,
// "angle": ...,
// "metadata": "...", // AI metadata of the building
// "actorSeed": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
// "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
// "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. May be INVALID_ENTITY.
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check whether we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
return false;
var foundationTemplate = "foundation|" + cmd.template;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity(foundationTemplate);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
return false;
}
// If it's a dock, get the right angle.
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
var angle = cmd.angle;
if (template.BuildRestrictions.PlacementType === "shore")
{
let angleDock = GetDockAngle(template, cmd.x, cmd.z);
if (angleDock !== undefined)
angle = angleDock;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(angle);
// Set the obstruction control group if needed
if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
{
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
// primary control group must always be valid
if (cmd.obstructionControlGroup)
{
if (cmd.obstructionControlGroup <= 0)
warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
}
if (cmd.obstructionControlGroup2)
cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (cmpBuildRestrictions)
{
var ret = cmpBuildRestrictions.CheckPlacement();
if (!ret.success)
{
if (g_DebugCommands)
warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
ret.players = [player];
cmpGuiInterface.PushNotification(ret);
// Remove the foundation because the construction was aborted
// move it out of world because it's not destroyed immediately.
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
}
else
error("cmpBuildRestrictions not defined");
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("The building's technology requirements are not met."),
"translateMessage": true
});
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
}
// We need the cost after tech and aura modifications.
let cmpCost = Engine.QueryInterface(ent, IID_Cost);
let costs = cmpCost.GetResourceCosts();
if (!cmpPlayer.TrySubtractResources(costs))
{
if (g_DebugCommands)
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(ent);
cmpPosition.MoveOutOfWorld();
return false;
}
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual && cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(cmd.template);
// send Metadata info if any
if (cmd.metadata)
Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued,
"pushFront": cmd.pushFront,
"formation": cmd.formation || undefined
});
}
return ent;
}
function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
{
// 'cmd' message structure:
// {
// "type": "construct-wall",
// "entities": [...], // entities that will be ordered to construct the wall (if applicable)
// "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
// {
// "template": "...", // one of the templates from the wallset
// "x": ...,
// "z": ...,
// "angle": ...,
// },
// ...
// ],
// "wallSet": {
// "templates": {
// "tower": // tower template name
// "long": // long wall segment template name
// ... // etc.
// },
// "maxTowerOverlap": ...,
// "minTowerOverlap": ...,
// },
// "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall
// "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
// }
if (cmd.pieces.length <= 0)
return;
if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
return;
}
if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
return;
}
// Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
// and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
// groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
// towers in the case of snapping). The towers themselves all keep their default unique control groups.
// To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
// it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
// first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
// the first tower encountered towards the ending side of the wall (if any).
// We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
// wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
//
// FIRST PASS:
// - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
// cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
// as the primary control group, thus allowing it to be built overlapping the previous piece.
// - If we encounter a new tower along the way (which will gain its own control group), do the following:
// o First build it using temporarily the same control group of the previous (non-tower) piece
// o Set the previous piece's secondary control group to the tower's entity ID
// o Restore the primary control group of the constructed tower back its original (unique) value.
// The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
// to be placed while overlapping the previous piece.
//
// SECOND PASS:
// - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
// time registering the right neighbouring tower in each non-tower piece.
// first pass; L -> R
var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
// the first wall piece can be built while overlapping it.
if (cmd.startSnappedEntity)
{
var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
if (!cmpSnappedStartObstruction)
{
error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
//warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
}
var i = 0;
var queued = cmd.queued;
var pieces = clone(cmd.pieces);
for (; i < pieces.length; ++i)
{
var piece = pieces[i];
// All wall pieces after the first must be queued.
if (i > 0 && !queued)
queued = true;
// 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
// start position snapping (implying that the first entity we build must be a tower)
if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
{
if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
{
error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
break;
}
}
var constructPieceCmd = {
"type": "construct",
"entities": cmd.entities,
"template": piece.template,
"x": piece.x,
"z": piece.z,
"angle": piece.angle,
"autorepair": cmd.autorepair,
"autocontinue": cmd.autocontinue,
"queued": queued,
// Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
// using the control group of the last tower (see comments above).
"obstructionControlGroup": lastTowerControlGroup,
};
// If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
// control group directly at construction time (instead of setting it in the second pass) to allow it to be built
// while overlapping the snapped entity.
if (i == pieces.length - 1 && cmd.endSnappedEntity)
{
var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (cmpEndSnappedObstruction)
constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
}
var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
if (pieceEntityId)
{
// wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
piece.ent = pieceEntityId;
// if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
if (piece.template == cmd.wallSet.templates.tower)
{
var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
var newTowerControlGroup = pieceEntityId;
if (i > 0)
{
//warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup);
var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction);
// TODO: ensure that cmpPreviousObstruction exists
// TODO: ensure that the previous obstruction does not yet have a secondary control group set
cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
}
// TODO: ensure that cmpTowerObstruction exists
cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
lastTowerIndex = i;
lastTowerControlGroup = newTowerControlGroup;
}
}
else // failed to build wall piece, abort
break;
}
var lastBuiltPieceIndex = i - 1;
var wallComplete = (lastBuiltPieceIndex == pieces.length - 1);
// At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
// Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
// as their secondary control groups.
lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// only start off with the ending side's snapped tower's control group if we were able to build the entire wall
if (cmd.endSnappedEntity && wallComplete)
{
var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (!cmpSnappedEndObstruction)
{
error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
}
for (var j = lastBuiltPieceIndex; j >= 0; --j)
{
var piece = pieces[j];
if (!piece.ent)
{
error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
continue;
}
var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
if (!cmpPieceObstruction)
{
error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
continue;
}
if (piece.template == cmd.wallSet.templates.tower)
{
// encountered a tower entity, update the last tower control group
lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
}
else
{
// Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
// Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
// dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
if (existingSecondaryControlGroup == INVALID_ENTITY)
{
if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
{
cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
}
}
else if (existingSecondaryControlGroup != lastTowerControlGroup)
{
error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
break;
}
}
}
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
let formation = ExtractFormations(ents);
for (let fid in formation.members)
{
let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate)
{
// If an individual was selected, remove it from any formation
// and command it individually.
if (ents.length == 1)
{
let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
RemoveFromFormation(ents);
return [ cmpUnitAI ];
}
let formationUnitAIs = [];
// Find what formations the selected entities are currently in,
// and default to that unless the formation is forced or it's the null formation
// (we want that to reset whatever formations units are in).
if (formationTemplate != NULL_FORMATION)
{
let formation = ExtractFormations(ents);
let formationIds = Object.keys(formation.members);
if (formationIds.length == 1)
{
// Selected units either belong to this formation or have no formation.
let fid = formationIds[0];
let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length &&
cmpFormation.GetMemberCount() == formation.entities.length)
{
cmpFormation.DeleteTwinFormations();
// The whole formation was selected, so reuse its controller for this command.
if (!forceTemplate || formationTemplate == formation.templates[fid])
{
formationTemplate = formation.templates[fid];
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
}
else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)];
}
else if (cmpFormation && !forceTemplate)
{
// Just reuse the template.
formationTemplate = formation.templates[fid];
}
}
else if (formationIds.length)
{
// Check if all entities share a common formation, if so reuse this template.
let template = formation.templates[formationIds[0]];
for (let i = 1; i < formationIds.length; ++i)
if (formation.templates[formationIds[i]] != template)
{
template = null;
break;
}
if (template && !forceTemplate)
formationTemplate = template;
}
}
// Separate out the units that don't support the chosen formation.
let formedUnits = [];
let nonformedUnitAIs = [];
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
continue;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION;
if (nullFormation || !cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate || NULL_FORMATION))
{
if (nullFormation && cmpUnitAI.GetFormationController())
cmpUnitAI.LeaveFormation(cmd.queued || false);
nonformedUnitAIs.push(cmpUnitAI);
}
else
formedUnits.push(ent);
}
if (nonformedUnitAIs.length == ents.length)
{
// No units support the formation.
return nonformedUnitAIs;
}
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller.
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
let formationSeparation = 60;
let clusters = ClusterEntities(formedUnits, formationSeparation);
let formationEnts = [];
for (let cluster of clusters)
{
RemoveFromFormation(cluster);
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
for (let ent of cluster)
nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI));
continue;
}
// Create the new controller.
let formationEnt = Engine.AddEntity(formationTemplate);
let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
for (let ent of formationEnts)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
}
}
return nonformedUnitAIs.concat(formationUnitAIs);
}
/**
* Group a list of entities in clusters via single-links
*/
function ClusterEntities(ents, separationDistance)
{
let clusters = [];
if (!ents.length)
return clusters;
let distSq = separationDistance * separationDistance;
let positions = [];
// triangular matrix with the (squared) distances between the different clusters
// the other half is not initialised
let matrix = [];
for (let i = 0; i < ents.length; ++i)
{
matrix[i] = [];
clusters.push([ents[i]]);
let cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
positions.push(cmpPosition.GetPosition2D());
for (let j = 0; j < i; ++j)
matrix[i][j] = positions[i].distanceToSquared(positions[j]);
}
while (clusters.length > 1)
{
// search two clusters that are closer than the required distance
let closeClusters = undefined;
for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i)
for (let j = i - 1; j >= 0 && !closeClusters; --j)
if (matrix[i][j] < distSq)
closeClusters = [i,j];
// if no more close clusters found, just return all found clusters so far
if (!closeClusters)
return clusters;
// make a new cluster with the entities from the two found clusters
let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
// calculate the minimum distance between the new cluster and all other remaining
// clusters by taking the minimum of the two distances.
let distances = [];
for (let i = 0; i < clusters.length; ++i)
{
let a = closeClusters[1];
let b = closeClusters[0];
if (i == a || i == b)
continue;
let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a];
let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b];
distances.push(Math.min(dist1, dist2));
}
// remove the rows and columns in the matrix for the merged clusters,
// and the clusters themselves from the cluster list
clusters.splice(closeClusters[0],1);
clusters.splice(closeClusters[1],1);
matrix.splice(closeClusters[0],1);
matrix.splice(closeClusters[1],1);
for (let i = 0; i < matrix.length; ++i)
{
if (matrix[i].length > closeClusters[0])
matrix[i].splice(closeClusters[0],1);
if (matrix[i].length > closeClusters[1])
matrix[i].splice(closeClusters[1],1);
}
// add a new row of distances to the matrix and the new cluster
clusters.push(newCluster);
matrix.push(distances);
}
return clusters;
}
function GetFormationRequirements(formationTemplate)
{
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate);
if (!template.Formation)
return false;
return { "minCount": +template.Formation.RequiredMemberCount };
}
function CanMoveEntsIntoFormation(ents, formationTemplate)
{
// TODO: should check the player's civ is allowed to use this formation
// See simulation/components/Player.js GetFormations() for a list of all allowed formations
var requirements = GetFormationRequirements(formationTemplate);
if (!requirements)
return false;
var count = 0;
for (let ent of ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate))
continue;
++count;
}
return count >= requirements.minCount;
}
/**
* Check if player can control this entity
* returns: true if the entity is owned by the player and controllable
* or control all units is activated, else false
*/
function CanControlUnit(entity, player, controlAll)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
let canBeControlled = IsOwnedByPlayer(player, entity) &&
(!cmpIdentity || cmpIdentity.IsControllable()) ||
controlAll;
if (!canBeControlled)
notifyOrderFailure(entity, player);
return canBeControlled;
}
/**
* @param {number} entity - The entityID to verify.
* @param {number} player - The playerID to check against.
* @return {boolean}.
*/
function IsOwnedByPlayerOrMutualAlly(entity, player)
{
return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity);
}
/**
* Check if player can control this entity
* @return {boolean} - True if the entity is valid and controlled by the player
* or the entity is owned by an mutualAlly and can be controlled
* or control all units is activated, else false.
*/
function CanPlayerOrAllyControlUnit(entity, player, controlAll)
{
return CanControlUnit(player, entity, controlAll) ||
IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity);
}
/**
* @return {boolean} - Whether the owner of this entity can control the entity.
*/
function CanOwnerControlEntity(entity)
{
let cmpOwner = QueryOwnerInterface(entity);
return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID());
}
/**
* Filter entities which the player can control.
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(ent => CanControlUnit(ent, player, controlAll));
}
/**
* Filter entities which the player can control or are mutualAlly
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll));
}
/**
* Incur the player with the cost of a bribe, optionally multiply the cost with
* the additionalMultiplier
*/
function IncurBribeCost(template, player, playerBribed, failedBribe)
{
let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed);
if (!cmpPlayerBribed)
return false;
let costs = {};
// Additional cost for this owner
let multiplier = cmpPlayerBribed.GetSpyCostMultiplier();
if (failedBribe)
multiplier *= template.VisionSharing.FailureCostRatio;
for (let res in template.Cost.Resources)
costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template));
let cmpPlayer = QueryPlayerIDInterface(player);
return cmpPlayer && cmpPlayer.TrySubtractResources(costs);
}
Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Engine.RegisterGlobal("g_Commands", g_Commands);
Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost);