Source: gui/session/selection_panels.js

/**
 * Contains the layout and button settings per selection panel
 *
 * getItems returns a list of basic items used to fill the panel.
 * This method is obligated. If the items list is empty, the panel
 * won't be rendered.
 *
 * Then there's a loop over all items provided. In the loop,
 * the item and some other standard data is added to a data object.
 *
 * The standard data is
 * var data = {
 *   "i":              index
 *   "item":           item coming from the getItems function
 *   "selection":      list of currently selected items
 *   "playerState":    playerState
 *   "unitEntState":   first selected entity state
 *   "rowLength":      rowLength
 *   "numberOfItems":  number of items that will be processed
 *   "button":         gui Button object
 *   "icon":           gui Icon object
 *   "guiSelection":   gui button Selection overlay
 *   "countDisplay":   gui caption space
 * };
 *
 * Then, addData is called, and can be used to abort the processing
 * of the current item by returning false.
 * It should return true if you want the panel to be filled.
 *
 * addData is used to add data to the data object on top
 * (or instead of) the standard data.
 * addData is not obligated, the function will just continue
 * with the content setters if no addData is present.
 *
 * After the addData, all functions starting with "set" are called.
 * These are used to set various parts of content.
 */

/* cache some formation info */
var g_availableFormations = new Map();   // available formations per player
var g_formationsInfo = new Map();

var g_SelectionPanels = {};

// ALERT
g_SelectionPanels.Alert = {
	"getMaxNumberOfItems": function()
	{
		return 2;
	},
	"getItems": function(unitEntState)
	{
		if (!unitEntState.alertRaiser)
			return [];
		return ["increase", "end"];
	},
	"setAction": function(data)
	{
		data.button.onPress = function() {
			if (data.item == "increase")
				increaseAlertLevel();
			else if (data.item == "end")
				endOfAlert();
		};
	},
	"setTooltip": function(data)
	{
		if (data.item == "increase")
		{
			if (data.unitEntState.alertRaiser.hasRaisedAlert)
				data.button.tooltip = translate("Increase the alert level to protect more units");
			else
				data.button.tooltip = translate("Raise an alert!");
		}
		else if (data.item == "end")
			data.button.tooltip = translate("End of alert.");
	},
	"setGraphics": function(data)
	{
		if (data.item == "increase")
		{
			data.button.hidden = !data.unitEntState.alertRaiser.canIncreaseLevel;
			if (data.unitEntState.alertRaiser.hasRaisedAlert)
				data.icon.sprite = "stretched:session/icons/bell_level2.png";
			else
				data.icon.sprite = "stretched:session/icons/bell_level1.png";
		}
		else if (data.item == "end")
		{
			data.button.hidden = !data.unitEntState.alertRaiser.hasRaisedAlert;
			data.icon.sprite = "stretched:session/icons/bell_level0.png";
		}
		data.button.enabled = !data.button.hidden && controlsPlayer(data.unitEntState.player);
	}
};

// BARTER
g_SelectionPanels.Barter = {
	"getMaxNumberOfItems": function()
	{
		return 4;
	},
	"rowLength": 4,
	"getItems": function(unitEntState, selection)
	{
		if (!unitEntState.barterMarket)
			return [];
		// ["food", "wood", "stone", "metal"]
		return BARTER_RESOURCES;
	},
	"addData": function(data)
	{
		// data.item is the resource name in this case
		data.button = {};
		data.icon = {};
		data.amount = {};
		for (var a of BARTER_ACTIONS)
		{
			data.button[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Button["+data.i+"]");
			data.icon[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Icon["+data.i+"]");
			data.amount[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Amount["+data.i+"]");
		}
		data.selectionIcon = Engine.GetGUIObjectByName("unitBarterSellSelection["+data.i+"]");

		data.amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL;
		if (Engine.HotkeyIsPressed("session.massbarter"))
			data.amountToSell *= BARTER_BUNCH_MULTIPLIER;
		data.isSelected = data.item == g_barterSell;
		return true;
	},
	"setCountDisplay": function(data)
	{
		data.amount.Sell.caption = "-" + data.amountToSell;
		var sellPrice = data.unitEntState.barterMarket.prices.sell[g_barterSell];
		var buyPrice = data.unitEntState.barterMarket.prices.buy[data.item];
		data.amount.Buy.caption = "+" + Math.round(sellPrice / buyPrice * data.amountToSell);
	},
	"setTooltip": function(data)
	{
		var resource = getLocalizedResourceName(data.item, "withinSentence");
		data.button.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource });
		data.button.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource });
	},
	"setAction": function(data)
	{
		data.button.Sell.onPress = function() { g_barterSell = data.item; };
		var exchangeResourcesParameters = {
			"sell": g_barterSell,
			"buy": data.item,
			"amount": data.amountToSell
		};
		data.button.Buy.onPress = function() { exchangeResources(exchangeResourcesParameters); };
	},
	"setGraphics": function(data)
	{
		var grayscale = data.isSelected ? "color: 0 0 0 100:grayscale:" : "";

		// do we have enough of this resource to sell?
		var neededRes = {};
		neededRes[data.item] = data.amountToSell;
		var canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", {
			"cost": neededRes,
			"player": data.unitEntState.player
		}) ? "color:255 0 0 80:" : "";

		// Let's see if we have enough resources to barter.
		neededRes = {};
		neededRes[g_barterSell] = data.amountToSell;
		var canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", {
			"cost": neededRes,
			"player": data.unitEntState.player
		}) ? "color:255 0 0 80:" : "";

		data.icon.Sell.sprite = canSellCurrent + "stretched:"+grayscale+"session/icons/resources/" + data.item + ".png";
		data.icon.Buy.sprite = canBuyAny + "stretched:"+grayscale+"session/icons/resources/" + data.item + ".png";

		data.button.Buy.hidden = data.isSelected;
		data.button.Buy.enabled = controlsPlayer(data.unitEntState.player);
		data.button.Sell.hidden = false;
		data.selectionIcon.hidden = !data.isSelected;
	},
	"setPosition": function(data)
	{
		setPanelObjectPosition(data.button.Sell, data.i, data.rowLength);
		setPanelObjectPosition(data.button.Buy, data.i + data.rowLength, data.rowLength);
	}
};

// COMMAND
g_SelectionPanels.Command = {
	"getMaxNumberOfItems": function()
	{
		return 6;
	},
	"getItems": function(unitEntState)
	{
		let commands = [];

		for (let c in g_EntityCommands)
		{
			var info = g_EntityCommands[c].getInfo(unitEntState);
			if (!info)
				continue;

			info.name = c;
			commands.push(info);
		}
		return commands;
	},
	"setTooltip": function(data)
	{
		data.button.tooltip = data.item.tooltip;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() {
			if (data.item.callback)
				data.item.callback(data.item);
			else
				performCommand(data.unitEntState.id, data.item.name);
		};
	},
	"setCountDisplay": function(data)
	{
		data.countDisplay.caption = data.item.count || "";
	},
	"setGraphics": function(data)
	{
		data.button.enabled = controlsPlayer(data.unitEntState.player);
		let grayscale = data.button.enabled ? "" : "grayscale:";
		data.icon.sprite = "stretched:" + grayscale + "session/icons/" + data.item.icon;
	},
	"setPosition": function(data)
	{
		var size = data.button.size;
		// count on square buttons, so size.bottom is the width too
		var spacer = size.bottom + 1;
		// relative to the center ( = 50%)
		size.rleft = size.rright = 50;
		// offset from the center calculation
		size.left = (data.i - data.numberOfItems/2) * spacer;
		size.right = size.left + size.bottom;
		data.button.size = size;
	}
};

//ALLY COMMAND
g_SelectionPanels.AllyCommand = {
	"getMaxNumberOfItems": function()
	{
		return 2;
	},
	"getItems": function(unitEntState)
	{
		var commands = [];
		for (var c in g_AllyEntityCommands)
		{
			var info = g_AllyEntityCommands[c].getInfo(unitEntState);
			if (!info)
				continue;
			info.name = c;
			commands.push(info);
		}
		return commands;
	},
	"setTooltip": function(data)
	{
		data.button.tooltip = data.item.tooltip;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() {
			if (data.item.callback)
				data.item.callback(data.item);
			else
				performAllyCommand(data.unitEntState.id, data.item.name);
		};
	},
	"conflictsWith": ["Command"],
	"setCountDisplay": function(data)
	{
		data.countDisplay.caption = data.item.count || "";
	},
	"setGraphics": function(data)
	{
		data.button.enabled = data.item.count != undefined && data.item.count > 0;
		let grayscale = data.button.enabled ? "" : "grayscale:";
		data.icon.sprite = "stretched:" + grayscale + "session/icons/" + data.item.icon;
	},
	"setPosition": function(data)
	{
		var size = data.button.size;
		// count on square buttons, so size.bottom is the width too
		var spacer = size.bottom + 1;
		// relative to the center ( = 50%)
		size.rleft = size.rright = 50;
		// offset from the center calculation
		size.left = (data.i - data.numberOfItems/2) * spacer;
		size.right = size.left + size.bottom;
		data.button.size = size;
	}
};

// CONSTRUCTION
g_SelectionPanels.Construction = {
	"getMaxNumberOfItems": function()
	{
		return 24 - getNumberOfRightPanelButtons();
	},
	"getItems": function()
	{
		return getAllBuildableEntitiesFromSelection();
	},
	"addData": function(data)
	{
		data.entType = data.item;
		data.template = GetTemplateData(data.entType);
		if (!data.template) // abort if no template
			return false;

		data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
			"tech": data.template.requiredTechnology,
			"player": data.unitEntState.player
		});

		if (data.template.cost)
			data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
				"cost": multiplyEntityCosts(data.template, 1),
				"player": data.unitEntState.player
			});

		data.limits = getEntityLimitAndCount(data.playerState, data.entType);

		if (data.template.wallSet)
			data.template.auras = GetTemplateData(data.template.wallSet.templates.long).auras;

		return true;
	},
	"setAction": function(data)
	{
		data.button.onPress = function () { startBuildingPlacement(data.item, data.playerState); };
	},
	"setTooltip": function(data)
	{
		var tooltip = getEntityNamesFormatted(data.template);
		tooltip += getVisibleEntityClassesFormatted(data.template);
		tooltip += getAurasTooltip(data.template);

		if (data.template.tooltip)
			tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]";

		tooltip += "\n" + getEntityCostTooltip(data.template);
		tooltip += getPopulationBonusTooltip(data.template);

		tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers);

		if (!data.technologyEnabled)
			tooltip += "\n" + sprintf(translate("Requires %(technology)s"), {
				"technology": getEntityNames(GetTechnologyData(data.template.requiredTechnology))
			});

		if (data.neededResources)
			tooltip += getNeededResourcesTooltip(data.neededResources);

		data.button.tooltip = tooltip;
		return true;
	},
	"setGraphics": function(data)
	{
		var modifier = "";
		if (!data.technologyEnabled || data.limits.canBeAddedCount == 0)
		{
			data.button.enabled = false;
			modifier += "color: 0 0 0 127:";
			modifier += "grayscale:";
		}
		else if (data.neededResources)
		{
			data.button.enabled = false;
			modifier += resourcesToAlphaMask(data.neededResources) +":";
		}
		else
			data.button.enabled = controlsPlayer(data.unitEntState.player);

		if (data.template.icon)
			data.icon.sprite = modifier + "stretched:session/portraits/" + data.template.icon;
	},
	"setPosition": function(data)
	{
		var index = data.i + getNumberOfRightPanelButtons();
		setPanelObjectPosition(data.button, index, data.rowLength);
	}
};

// FORMATION
g_SelectionPanels.Formation = {
	"getMaxNumberOfItems": function()
	{
		return 16;
	},
	"rowLength": 4,
	"conflictsWith": ["Garrison"],
	"getItems": function(unitEntState)
	{
		if (!hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal"))
			return [];
		if (!g_availableFormations.has(unitEntState.player))
			g_availableFormations.set(unitEntState.player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntState.player));
		return g_availableFormations.get(unitEntState.player);
	},
	"addData": function(data)
	{
		if (!g_formationsInfo.has(data.item))
			g_formationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));
		data.formationInfo = g_formationsInfo.get(data.item);
		data.formationOk = canMoveSelectionIntoFormation(data.item);
		data.formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
			"ents": data.selection,
			"formationTemplate": data.item
		});
		return true;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() { performFormation(data.unitEntState.id, data.item); };
	},
	"setTooltip": function(data)
	{
		var tooltip = translate(data.formationInfo.name);
		if (!data.formationOk && data.formationInfo.tooltip)
			tooltip += "\n" + "[color=\"red\"]" + translate(data.formationInfo.tooltip) + "[/color]";
		data.button.tooltip = tooltip;
	},
	"setGraphics": function(data)
	{
		data.button.enabled = data.formationOk && controlsPlayer(data.unitEntState.player);
		var grayscale = data.formationOk ? "" : "grayscale:";
		data.guiSelection.hidden = !data.formationSelected;
		data.icon.sprite = "stretched:"+grayscale+"session/icons/"+data.formationInfo.icon;
	}
};

// GARRISON
g_SelectionPanels.Garrison = {
	"getMaxNumberOfItems": function()
	{
		return 12;
	},
	"rowLength": 4,
	"getItems": function(unitEntState, selection)
	{
		if (!unitEntState.garrisonHolder)
			return [];
		var groups = new EntityGroups();
		for (var ent of selection)
		{
			var state = GetEntityState(ent);
			if (state.garrisonHolder)
				groups.add(state.garrisonHolder.entities);
		}
		return groups.getEntsGrouped();
	},
	"addData": function(data)
	{
		data.entType = data.item.template;
		data.template = GetTemplateData(data.entType);
		if (!data.template)
			return false;
		data.name = getEntityNames(data.template);
		data.count = data.item.ents.length;
		return true;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() { unloadTemplate(data.item.template); };
	},
	"setTooltip": function(data)
	{
		var tooltip = sprintf(translate("Unload %(name)s"), { "name": data.name }) + "\n";
		tooltip += translate("Single-click to unload 1. Shift-click to unload all of this type.");
		data.button.tooltip = tooltip;
	},
	"setCountDisplay": function(data)
	{
		data.countDisplay.caption = data.count || "";
	},
	"setGraphics": function(data)
	{
		var grayscale = "";
		var ents = data.item.ents;
		var entplayer = GetEntityState(ents[0]).player;
		data.button.sprite = "color:" + rgbToGuiColor(g_Players[entplayer].color) +":";

		if (!controlsPlayer(data.unitEntState.player) && !controlsPlayer(entplayer))
		{
			data.button.enabled = false;
			grayscale = "grayscale:";
		}

		data.icon.sprite = "stretched:" + grayscale + "session/portraits/" + data.template.icon;
	}
};

// GATE
g_SelectionPanels.Gate = {
	"getMaxNumberOfItems": function()
	{
		return 24 - getNumberOfRightPanelButtons();
	},
	"getItems": function(unitEntState, selection)
	{
		// Allow long wall pieces to be converted to gates
		var longWallTypes = {};
		var walls = [];
		var gates = [];
		for (var ent of selection)
		{
			var state = GetEntityState(ent);
			if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template])
			{
				var gateTemplate = getWallGateTemplate(state.id);
				if (gateTemplate)
				{
					var tooltipString = GetTemplateDataWithoutLocalization(state.template).gateConversionTooltip;
					if (!tooltipString)
					{
						warn(state.template + " is supposed to be convertable to a gate, but it's missing the GateConversionTooltip in the Identity template");
						tooltipString = "";
					}
					walls.push({
						"tooltip": translate(tooltipString),
						"template": gateTemplate,
						"callback": function (item) { transformWallToGate(item.template); }
					});
				}

				// We only need one entity per type.
				longWallTypes[state.template] = true;
			}
			else if (state.gate && !gates.length)
			{
				gates.push({
					"gate": state.gate,
					"tooltip": translate("Lock Gate"),
					"locked": true,
					"callback": function (item) { lockGate(item.locked); }
				});
				gates.push({
					"gate": state.gate,
					"tooltip": translate("Unlock Gate"),
					"locked": false,
					"callback": function (item) { lockGate(item.locked); }
				});
			}
			// Show both 'locked' and 'unlocked' as active if the selected gates have both lock states.
			else if (state.gate && state.gate.locked != gates[0].gate.locked)
				for (var j = 0; j < gates.length; ++j)
					delete gates[j].gate.locked;
		}

		// Place wall conversion options after gate lock/unlock icons.
		var items = gates.concat(walls);
		return items;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() {data.item.callback(data.item); };
	},
	"setTooltip": function(data)
	{
		var tooltip = data.item.tooltip;
		if (data.item.template)
		{
			data.template = GetTemplateData(data.item.template);
			data.wallCount = data.selection.reduce(function (count, ent) {
					var state = GetEntityState(ent);
					if (hasClass(state, "LongWall") && !state.gate)
						++count;
					return count;
				}, 0);

			tooltip += "\n" + getEntityCostTooltip(data.template, data.wallCount);

			data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
				"cost": multiplyEntityCosts(data.template, data.wallCount)
			});

			if (data.neededResources)
				tooltip += getNeededResourcesTooltip(data.neededResources);
		}
		data.button.tooltip = tooltip;
	},
	"setGraphics": function(data)
	{
		data.button.enabled = controlsPlayer(data.unitEntState.player);
		var gateIcon;
		if (data.item.gate)
		{
			// If already a gate, show locking actions
			gateIcon = "icons/lock_" + GATE_ACTIONS[data.item.locked ? 0 : 1] + "ed.png";
			if (data.item.gate.locked === undefined)
				data.guiSelection.hidden = false;
			else
				data.guiSelection.hidden = data.item.gate.locked != data.item.locked;
		}
		else
		{
			// otherwise show gate upgrade icon
			var template = GetTemplateData(data.item.template);
			if (!template)
				return;
			gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png";
			data.guiSelection.hidden = true;
		}

		data.icon.sprite = (data.neededResources ? resourcesToAlphaMask(data.neededResources) + ":" : "") + "stretched:session/" + gateIcon;
	},
	"setPosition": function(data)
	{
		var index = data.i + getNumberOfRightPanelButtons();
		setPanelObjectPosition(data.button, index, data.rowLength);
	}
};

// PACK
g_SelectionPanels.Pack = {
	"getMaxNumberOfItems": function()
	{
		return 24 - getNumberOfRightPanelButtons();
	},
	"getItems": function(unitEntState, selection)
	{
		var checks = {};
		for (var ent of selection)
		{
			var state = GetEntityState(ent);
			if (!state.pack)
				continue;
			if (state.pack.progress == 0)
			{
				if (!state.pack.packed)
					checks.packButton = true;
				else if (state.pack.packed)
					checks.unpackButton = true;
			}
			else
			{
				// Already un/packing - show cancel button
				if (!state.pack.packed)
					checks.packCancelButton = true;
				else if (state.pack.packed)
					checks.unpackCancelButton = true;
			}
		}
		var items = [];
		if (checks.packButton)
			items.push({ "packing": false, "packed": false, "tooltip": translate("Pack"), "callback": function() { packUnit(true); } });
		if (checks.unpackButton)
			items.push({ "packing": false, "packed": true, "tooltip": translate("Unpack"), "callback": function() { packUnit(false); } });
		if (checks.packCancelButton)
			items.push({ "packing": true, "packed": false, "tooltip": translate("Cancel Packing"), "callback": function() { cancelPackUnit(true); } });
		if (checks.unpackCancelButton)
			items.push({ "packing": true, "packed": true, "tooltip": translate("Cancel Unpacking"), "callback": function() { cancelPackUnit(false); } });
		return items;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() {data.item.callback(data.item); };
	},
	"setTooltip": function(data)
	{
		data.button.tooltip = data.item.tooltip;
	},
	"setGraphics": function(data)
	{
		if (data.item.packing)
			data.icon.sprite = "stretched:session/icons/cancel.png";
		else if (data.item.packed)
			data.icon.sprite = "stretched:session/icons/unpack.png";
		else
			data.icon.sprite = "stretched:session/icons/pack.png";

		data.button.enabled = controlsPlayer(data.unitEntState.player);
	},
	"setPosition": function(data)
	{
		var index = data.i + getNumberOfRightPanelButtons();
		setPanelObjectPosition(data.button, index, data.rowLength);
	}
};

// QUEUE
g_SelectionPanels.Queue = {
	"getMaxNumberOfItems": function()
	{
		return 16;
	},
	"getItems": function(unitEntState, selection)
	{
		return getTrainingQueueItems(selection);
	},
	"resizePanel": function(numberOfItems, rowLength)
	{
		var numRows = Math.ceil(numberOfItems / rowLength);
		var panel = Engine.GetGUIObjectByName("unitQueuePanel");
		var size = panel.size;
		var buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom;
		var margin = 4;
		size.top = size.bottom - numRows*buttonSize - (numRows+2)*margin;
		panel.size = size;
	},
	"addData": function(data)
	{
		// differentiate between units and techs
		if (data.item.unitTemplate)
		{
			data.entType = data.item.unitTemplate;
			data.template = GetTemplateData(data.entType);
		}
		else if (data.item.technologyTemplate)
		{
			data.entType = data.item.technologyTemplate;
			data.template = GetTechnologyData(data.entType);
		}
		data.progress = Math.round(data.item.progress*100) + "%";

		return data.template;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, data.item.id); };
	},
	"setTooltip": function(data)
	{
		var tooltip = getEntityNames(data.template);
		if (data.item.neededSlots)
		{
			tooltip += "\n[color=\"red\"]" + translate("Insufficient population capacity:") + "\n[/color]";
			tooltip += sprintf(translate("%(population)s %(neededSlots)s"), {
				"population": getCostComponentDisplayName("population"),
				"neededSlots": data.item.neededSlots
			});
		}
		data.button.tooltip = tooltip;
	},
	"setCountDisplay": function(data)
	{
		data.countDisplay.caption = data.item.count > 1 ? data.item.count : "";
	},
	"setProgressDisplay": function(data)
	{
		// show the progress number for the first item
		if (data.i == 0)
			Engine.GetGUIObjectByName("queueProgress").caption = data.progress;

		var guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider["+data.i+"]");
		var size = guiObject.size;

		// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
		size.top = size.left + Math.round(data.item.progress * (size.right - size.left));
		guiObject.size = size;
	},
	"setGraphics": function(data)
	{
		if (data.template.icon)
			data.icon.sprite = "stretched:session/portraits/" + data.template.icon;

		data.button.enabled = controlsPlayer(data.unitEntState.player);
	}
};

// RESEARCH
g_SelectionPanels.Research = {
	"getMaxNumberOfItems": function()
	{
		return 8;
	},
	"getItems": function(unitEntState, selection)
	{
		// TODO 8 is the row lenght, make variable
		if (getNumberOfRightPanelButtons() > 8 && selection.length > 1)
			return [];
		for (var ent of selection)
		{
			var entState = GetEntityState(ent);
			if (entState.production && entState.production.technologies.length)
				return entState.production.technologies;
		}
		return [];
	},
	"hideItem": function(i, rowLength) // called when no item is found
	{
		Engine.GetGUIObjectByName("unitResearchButton["+i+"]").hidden = true;
		// We also remove the paired tech and the pair symbol
		Engine.GetGUIObjectByName("unitResearchButton["+(i+rowLength)+"]").hidden = true;
		Engine.GetGUIObjectByName("unitResearchPair["+i+"]").hidden = true;
	},
	"addData": function(data)
	{
		data.entType = data.item.pair ? [data.item.top, data.item.bottom] : [data.item];
		data.template = data.entType.map(GetTechnologyData);
		// abort if no template found for any of the techs
		if (data.template.some(v => !v))
			return false;
		// index one row below
		var shiftedIndex = data.i + data.rowLength;
		data.positions = data.item.pair ? [data.i, shiftedIndex] : [shiftedIndex];
		data.positionsToHide = data.item.pair ? [] : [data.i];

		// add top buttons to the data
		data.button = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchButton["+p+"]"));
		data.buttonsToHide = data.positionsToHide.map(p => Engine.GetGUIObjectByName("unitResearchButton["+p+"]"));

		data.icon = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchIcon["+p+"]"));
		data.unchosenIcon = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchUnchosenIcon["+p+"]"));

		data.neededResources = data.template.map(t => Engine.GuiInterfaceCall("GetNeededResources", {
			"cost": t.cost,
			"player": data.unitEntState.player
		}));

		data.requirementsPassed = data.entType.map(e => Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
			"tech": e,
			"player": data.unitEntState.player
		}));

		data.pair = Engine.GetGUIObjectByName("unitResearchPair["+data.i+"]");

		return true;
	},
	"setTooltip": function(data)
	{
		for (var i in data.entType)
		{
			var tooltip = "";
			var template = data.template[i];
			tooltip = getEntityNamesFormatted(template);
			if (template.tooltip)
				tooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]";

			tooltip += "\n" + getEntityCostTooltip(template);
			if (!data.requirementsPassed[i])
			{
				tooltip += "\n" + template.requirementsTooltip;
				if (template.classRequirements)
				{
					var player = data.unitEntState.player;
					var current = GetSimState().players[player].classCounts[template.classRequirements.class] || 0;
					var remaining = template.classRequirements.number - current;
					tooltip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), { "number": remaining });
				}
			}
			if (data.neededResources[i])
				tooltip += getNeededResourcesTooltip(data.neededResources[i]);
			data.button[i].tooltip = tooltip;
		}
	},
	"setAction": function(data)
	{
		for (var i in data.entType)
		{
			// array containing the indices other buttons
			var others = Object.keys(data.template);
			others.splice(i, 1);
			var button = data.button[i];
			// as we're in a loop, we need to limit the scope with a closure
			// else the last value of the loop will be taken, rather than the current one
			button.onpress = (function(template) { return function () { addResearchToQueue(data.unitEntState.id, template); }; })(data.entType[i]);
			// on mouse enter, show a cross over the other icons
			button.onmouseenter = (function(others, icons) {
				return function() {
					for (var j of others)
						icons[j].hidden = false;
				};
			})(others, data.unchosenIcon);
			button.onmouseleave = (function(others, icons) {
				return function() {
					for (var j of others)
						icons[j].hidden = true;
				};
			})(others, data.unchosenIcon);
		}
	},
	"setGraphics": function(data)
	{
		for (var i in data.entType)
		{
			let button = data.button[i];
			button.hidden = false;
			var modifier = "";
			if (!data.requirementsPassed[i])
			{
				button.enabled = false;
				modifier += "color: 0 0 0 127:";
				modifier += "grayscale:";
			}
			else if (data.neededResources[i])
			{
				button.enabled = false;
				modifier += resourcesToAlphaMask(data.neededResources[i]) + ":";
			}
			else
				button.enabled = controlsPlayer(data.unitEntState.player);

			if (data.template[i].icon)
				data.icon[i].sprite = modifier + "stretched:session/portraits/" + data.template[i].icon;
		}
		for (let button of data.buttonsToHide)
			button.hidden = true;
		// show the tech connector
		data.pair.hidden = data.item.pair == null;
	},
	"setPosition": function(data)
	{
		for (var i in data.button)
			setPanelObjectPosition(data.button[i], data.positions[i], data.rowLength);
		setPanelObjectPosition(data.pair, data.i, data.rowLength);
	}
};

// SELECTION
g_SelectionPanels.Selection = {
	"getMaxNumberOfItems": function()
	{
		return 16;
	},
	"rowLength": 4,
	"getItems": function(unitEntState, selection)
	{
		if (selection.length < 2)
			return [];
		return g_Selection.groups.getTemplateNames();
	},
	"addData": function(data)
	{
		data.entType = data.item;
		data.template = GetTemplateData(data.entType);
		if (!data.template)
			return false;
		data.name = getEntityNames(data.template);

		var ents = g_Selection.groups.getEntsByName(data.item);
		data.count = ents.length;
		for (var ent of ents)
		{
			var state = GetEntityState(ent);

			if (state.resourceCarrying && state.resourceCarrying.length !== 0)
			{
				if (!data.carried)
					data.carried = {};
				var carrying = state.resourceCarrying[0];
				if (data.carried[carrying.type])
					data.carried[carrying.type] += carrying.amount;
				else
					data.carried[carrying.type] = carrying.amount;
			}

			if (state.trader && state.trader.goods && state.trader.goods.amount)
			{
				if (!data.carried)
					data.carried = {};
				var amount = state.trader.goods.amount;
				var type = state.trader.goods.type;
				var totalGain = amount.traderGain;
				if (amount.market1Gain)
					totalGain += amount.market1Gain;
				if (amount.market2Gain)
					totalGain += amount.market2Gain;
				if (data.carried[type])
					data.carried[type] += totalGain;
				else
					data.carried[type] = totalGain;
			}
		}
		return true;
	},
	"setTooltip": function(data)
	{
		if (data.carried)
		{
			var str = data.name + "\n";
			var ress = ["food", "wood", "stone", "metal"];
			for (var i = 0; i < 4; ++i)
			{
				if (data.carried[ress[i]])
				{
					str += getCostComponentDisplayName(ress[i]) + data.carried[ress[i]];
					if (i !== 3)
						str += " ";
				}
			}
			data.button.tooltip = str;
		}
		else
			data.button.tooltip = data.name;
	},
	"setCountDisplay": function(data)
	{
		data.countDisplay.caption = data.count || "";
	},
	"setAction": function(data)
	{
		data.button.onpressright = function() { changePrimarySelectionGroup(data.item, true); };
		data.button.onpress = function() { changePrimarySelectionGroup(data.item, false); };
	},
	"setGraphics": function(data)
	{
		if (data.template.icon)
			data.icon.sprite = "stretched:session/portraits/" + data.template.icon;
	}
};

// STANCE
g_SelectionPanels.Stance = {
	"getMaxNumberOfItems": function()
	{
		return 5;
	},
	"getItems": function(unitEntState)
	{
		if (!unitEntState.unitAI || !hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal"))
			return [];
		return unitEntState.unitAI.possibleStances;
	},
	"addData": function(data)
	{
		data.stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", {
			"ents": data.selection,
			"stance": data.item
		});
		return true;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() { performStance(data.unitEntState, data.item); };
	},
	"setTooltip": function(data)
	{
		data.button.tooltip = getStanceDisplayName(data.item) + "\n[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]";
	},
	"setGraphics": function(data)
	{
		data.guiSelection.hidden = !data.stanceSelected;
		data.icon.sprite = "stretched:session/icons/stances/"+data.item+".png";
		data.button.enabled = controlsPlayer(data.unitEntState.player);
	}
};

// TRAINING
g_SelectionPanels.Training = {
	"getMaxNumberOfItems": function()
	{
		return 24 - getNumberOfRightPanelButtons();
	},
	"getItems": function()
	{
		return getAllTrainableEntitiesFromSelection();
	},
	"addData": function(data)
	{
		data.entType = data.item;
		data.template = GetTemplateData(data.entType);
		if (!data.template)
			return false;

		data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
			"tech": data.template.requiredTechnology,
			"player": data.unitEntState.player
		});

		var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
			getTrainingBatchStatus(data.playerState, data.unitEntState.id, data.entType, data.selection);

		data.buildingsCountToTrainFullBatch = buildingsCountToTrainFullBatch;
		data.fullBatchSize = fullBatchSize;
		data.remainderBatch = remainderBatch;

		data.trainNum = buildingsCountToTrainFullBatch || 1; // train at least one unit
		if (Engine.HotkeyIsPressed("session.batchtrain"))
			data.trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;

		if (data.template.cost)
			data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
				"cost": multiplyEntityCosts(data.template, data.trainNum),
				"player": data.unitEntState.player
			});

		return true;
	},
	"setAction": function(data)
	{
		data.button.onPress = function() { addTrainingToQueue(data.selection, data.item, data.playerState); };
	},
	"setCountDisplay": function(data)
	{
		data.countDisplay.caption = data.trainNum > 1 ? data.trainNum : "";
	},
	"setTooltip": function(data)
	{
		var tooltip = "";
		var key = Engine.ConfigDB_GetValue("user", "hotkey.session.queueunit." + (data.i + 1));
		if (key)
			tooltip += "[color=\"255 251 131\"][font=\"sans-bold-16\"]\\[" + key + "][/font][/color] ";

		tooltip += getEntityNamesFormatted(data.template);
		tooltip += getVisibleEntityClassesFormatted(data.template);
		tooltip += getAurasTooltip(data.template);

		if (data.template.tooltip)
			tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]";

		tooltip += "\n" + getEntityCostTooltip(data.template, data.trainNum, data.unitEntState.id);

		data.limits = getEntityLimitAndCount(data.playerState, data.entType);

		tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers);
		if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
		{
			if (data.template.health)
				tooltip += "\n[font=\"sans-bold-13\"]" + translate("Health:") + "[/font] " + data.template.health;
			if (data.template.attack)
				tooltip += "\n" + getAttackTooltip(data.template);
			if (data.template.armour)
				tooltip += "\n" + getArmorTooltip(data.template.armour);
			if (data.template.speed)
				tooltip += "\n" + getSpeedTooltip(data.template);
		}
		tooltip += "[color=\"255 251 131\"]" + formatBatchTrainingString(data.buildingsCountToTrainFullBatch, data.fullBatchSize, data.remainderBatch) + "[/color]";
		if (!data.technologyEnabled)
		{
			var techName = getEntityNames(GetTechnologyData(data.template.requiredTechnology));
			tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { "technology": techName });
		}
		if (data.neededResources)
			tooltip += getNeededResourcesTooltip(data.neededResources);

		data.button.tooltip = tooltip;
	},
	// disable and enable buttons in the same way as when you do for the construction
	"setGraphics": g_SelectionPanels.Construction.setGraphics,
	"setPosition": function(data)
	{
		var index = data.i + getNumberOfRightPanelButtons();
		setPanelObjectPosition(data.button, index, data.rowLength);
	}
};



/**
 * If two panels need the same space, so they collide,
 * the one appearing first in the order is rendered.
 *
 * Note that the panel needs to appear in the list to get rendered.
 */
var g_PanelsOrder = [
	// LEFT PANE
	"Barter", // must always be visible on markets
	"Garrison", // more important than Formation, as you want to see the garrisoned units in ships
	"Alert",
	"Formation",
	"Stance", // normal together with formation

	// RIGHT PANE
	"Gate", // must always be shown on gates
	"Pack", // must always be shown on packable entities
	"Training",
	"Construction",
	"Research", // normal together with training

	// UNIQUE PANES (importance doesn't matter)
	"Command",
	"AllyCommand",
	"Queue",
	"Selection",
];