Correctly handle commands and production for multiple units and mixed selections. Patch by Imarok, reviewed by Itms, fixes #3823. Fixes #3492, #3288, #3668.

This was SVN commit r18773.
This commit is contained in:
elexis 2016-09-25 19:38:10 +00:00
parent a867fca514
commit a754b1bad4
8 changed files with 269 additions and 230 deletions

View file

@ -6,7 +6,7 @@
</object>
<object hotkey="camera.rallypointfocus">
<action on="Press">performCommand(g_Selection.getFirstSelected(), "focus-rally");</action>
<action on="Press">performCommand(GetExtendedEntityState(g_Selection.getFirstSelected()), "focus-rally");</action>
</object>
<!-- Camera jumping - press a hotkey to mark a position and another hotkey to jump back there -->

View file

@ -43,7 +43,7 @@
</object>
<object hotkey="session.kill">
<action on="Press">performCommand(g_Selection.getFirstSelected(), "delete");</action>
<action on="Press">performCommand(GetExtendedEntityState(g_Selection.getFirstSelected()), "delete");</action>
</object>
<object hotkey="session.unload">

View file

@ -228,7 +228,7 @@ function getActionInfo(action, target)
if (unitActions[action] && unitActions[action].getActionInfo)
{
var r = unitActions[action].getActionInfo(entState, targetState, simState);
if (r) // return true if it's possible for one of the entities
if (r && r.possible) // return true if it's possible for one of the entities
return r;
}
}
@ -1452,20 +1452,21 @@ function addResearchToQueue(entity, researchType)
Engine.PostNetworkCommand({ "type": "research", "entity": entity, "template": researchType });
}
// Returns the number of units that will be present in a batch if the user clicks
// the training button with shift down
function getTrainingBatchStatus(playerState, entity, trainEntType, selection)
/**
* Returns the number of units that will be present in a batch if the user clicks
* the training button with shift down
*/
function getTrainingBatchStatus(playerState, trainEntType, selection)
{
let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
var appropriateBuildings = [entity];
if (selection && selection.indexOf(entity) != -1)
var appropriateBuildings = [];
if (selection)
appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
var nextBatchTrainingCount = 0;
var currentBatchTrainingCount = 0;
var limits;
if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 &&
batchTrainingType == trainEntType)
if (inputState == INPUT_BATCHTRAINING && batchTrainingType == trainEntType)
{
nextBatchTrainingCount = batchTrainingCount;
currentBatchTrainingCount = batchTrainingCount;
@ -1507,11 +1508,10 @@ function changePrimarySelectionGroup(templateName, deselectGroup)
g_Selection.makePrimarySelection(templateName, Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup);
}
function performCommand(entity, commandName)
function performCommand(entState, commandName)
{
if (!entity)
if (!entState)
return;
var entState = GetExtendedEntityState(entity);
if (!controlsPlayer(entState.player) &&
!(g_IsObserver && commandName == "focus-rally"))
@ -1535,12 +1535,12 @@ function performAllyCommand(entity, commandName)
g_AllyEntityCommands[commandName].execute(entState);
}
function performFormation(entity, formationTemplate)
function performFormation(entities, formationTemplate)
{
if (entity)
if (entities)
Engine.PostNetworkCommand({
"type": "formation",
"entities": g_Selection.toList(),
"entities": entities,
"name": formationTemplate
});
}
@ -1582,17 +1582,14 @@ function performGroup(action, groupId)
}
}
function performStance(entity, stanceName)
function performStance(entities, stanceName)
{
if (entity)
{
var selection = g_Selection.toList();
if (entities)
Engine.PostNetworkCommand({
"type": "stance",
"entities": selection,
"entities": entities,
"name": stanceName
});
}
}
function lockGate(lock)
@ -1817,10 +1814,22 @@ function removeGuard()
Engine.PostNetworkCommand({ "type": "remove-guard", "entities": entities });
}
function raiseAlert()
{
let entities = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && state.alertRaiser && !state.alertRaiser.hasRaisedAlert;
});
Engine.PostNetworkCommand({ "type": "increase-alert-level", "entities": entities });
}
function increaseAlertLevel()
{
var entities = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
raiseAlert();
let entities = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && state.alertRaiser && state.alertRaiser.canIncreaseLevel;
});
@ -1829,8 +1838,8 @@ function increaseAlertLevel()
function endOfAlert()
{
var entities = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
let entities = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && state.alertRaiser && state.alertRaiser.hasRaisedAlert;
});
@ -1849,35 +1858,3 @@ function clearSelection()
preSelectedAction = ACTION_NONE;
}
/**
* Returns a list of all items in the productionqueue of the selection
* @param selection List with entity ids
*/
function getTrainingQueueItems(selection)
{
var entStates = [];
for (var ent of selection)
{
var entState = GetEntityState(ent);
if (entState.production)
entStates.push(entState);
}
var queue = [];
var i = 0;
do
{
var foundNewItems = false;
for (entState of entStates)
{
if (!entState.production.queue[i])
continue;
var item = entState.production.queue[i];
item.producingEnt = entState.id;
queue.push(item);
foundNewItems = true;
}
++i;
}
while (foundNewItems);
return queue;
}

View file

@ -20,14 +20,14 @@ function getResourceTypeDisplayName(resourceType)
}
// Updates the health bar of garrisoned units
function updateGarrisionHealthBar(entState, selection)
function updateGarrisonHealthBar(entState, selection)
{
if (!entState.garrisonHolder)
return;
// Summing up the Health of every single unit
let totalGarrisionHealth = 0;
let maxGarrisionHealth = 0;
let totalGarrisonHealth = 0;
let maxGarrisonHealth = 0;
for (let selEnt of selection)
{
let selEntState = GetEntityState(selEnt);
@ -35,24 +35,24 @@ function updateGarrisionHealthBar(entState, selection)
for (let ent of selEntState.garrisonHolder.entities)
{
let state = GetEntityState(ent);
totalGarrisionHealth += state.hitpoints || 0;
maxGarrisionHealth += state.maxHitpoints || 0;
totalGarrisonHealth += state.hitpoints || 0;
maxGarrisonHealth += state.maxHitpoints || 0;
}
}
// Configuring the health bar
let healthGarrison = Engine.GetGUIObjectByName("healthGarrison");
healthGarrison.hidden = totalGarrisionHealth <= 0;
if (totalGarrisionHealth > 0)
healthGarrison.hidden = totalGarrisonHealth <= 0;
if (totalGarrisonHealth > 0)
{
let healthBarGarrison = Engine.GetGUIObjectByName("healthBarGarrison");
let healthSize = healthBarGarrison.size;
healthSize.rtop = 100-100*Math.max(0, Math.min(1, totalGarrisionHealth / maxGarrisionHealth));
healthSize.rtop = 100-100*Math.max(0, Math.min(1, totalGarrisonHealth / maxGarrisonHealth));
healthBarGarrison.size = healthSize;
healthGarrison.tooltip = sprintf(translate("%(label)s %(current)s / %(max)s"), {
"label": "[font=\"sans-bold-13\"]" + translate("Hitpoints:") + "[/font]",
"current": Math.ceil(totalGarrisionHealth),
"max": Math.ceil(maxGarrisionHealth)
"current": Math.ceil(totalGarrisonHealth),
"max": Math.ceil(maxGarrisonHealth)
});
}
}
@ -313,7 +313,7 @@ function displaySingle(entState)
}
// Fills out information for multiple entities
function displayMultiple(selection)
function displayMultiple(entStates)
{
let averageHealth = 0;
let maxHealth = 0;
@ -323,11 +323,8 @@ function displayMultiple(selection)
let totalCarrying = {};
let totalLoot = {};
for (let i = 0; i < selection.length; ++i)
for (let entState of entStates)
{
let entState = GetExtendedEntityState(selection[i]);
if (!entState)
continue;
playerID = entState.player; // trust that all selected entities have the same owner
if (entState.hitpoints)
{
@ -403,7 +400,7 @@ function displayMultiple(selection)
}
let numberOfUnits = Engine.GetGUIObjectByName("numberOfUnits");
numberOfUnits.caption = selection.length;
numberOfUnits.caption = entStates.length;
numberOfUnits.tooltip = "";
if (Object.keys(totalCarrying).length)
@ -434,9 +431,17 @@ function updateSelectionDetails()
let detailsPanel = Engine.GetGUIObjectByName("selectionDetails");
let commandsPanel = Engine.GetGUIObjectByName("unitCommands");
let selection = g_Selection.toList();
let entStates = [];
if (selection.length == 0)
for (let sel of g_Selection.toList())
{
let entState = GetExtendedEntityState(sel);
if (!entState)
continue;
entStates.push(entState);
}
if (entStates.length == 0)
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
@ -448,26 +453,19 @@ function updateSelectionDetails()
return;
}
/* If the unit has no data (e.g. it was killed), don't try displaying any
data for it. (TODO: it should probably be removed from the selection too;
also need to handle multi-unit selections) */
let entState = GetExtendedEntityState(selection[0]);
if (!entState)
return;
// Fill out general info and display it
if (selection.length == 1)
displaySingle(entState);
if (entStates.length == 1)
displaySingle(entStates[0]);
else
displayMultiple(selection);
displayMultiple(entStates);
// Show basic details.
detailsPanel.hidden = false;
// Fill out commands panel for specific unit selected (or first unit of primary group)
updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection);
updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel);
// Show health bar for garrisoned units if the garrison panel is visible
if (Engine.GetGUIObjectByName("unitGarrisonPanel") && !Engine.GetGUIObjectByName("unitGarrisonPanel").hidden)
updateGarrisionHealthBar(entState, selection);
updateGarrisonHealthBar(entStates[0], g_Selection.toList());
}

View file

@ -12,9 +12,8 @@
* {
* "i": index
* "item": item coming from the getItems function
* "selection": list of currently selected items
* "playerState": playerState
* "unitEntState": first selected entity state
* "unitEntStates": states of the selected entities
* "rowLength": rowLength
* "numberOfItems": number of items that will be processed
* "button": gui Button object
@ -37,49 +36,59 @@ let g_SelectionPanels = {};
g_SelectionPanels.Alert = {
"getMaxNumberOfItems": function()
{
return 2;
return 3;
},
"getItems": function(unitEntState)
"conflictsWith": ["Barter"],
"getItems": function(unitEntStates)
{
if (!unitEntState.alertRaiser)
return [];
return ["increase", "end"];
let ret = [];
if (unitEntStates.some(state => state.alertRaiser && !state.alertRaiser.hasRaisedAlert))
ret.push("raise");
if (unitEntStates.some(state => state.alertRaiser && state.alertRaiser.hasRaisedAlert && state.alertRaiser.canIncreaseLevel))
ret.push("increase");
if (unitEntStates.some(state => state.alertRaiser && state.alertRaiser.hasRaisedAlert))
ret.push("end");
return ret;
},
"setupButton": function(data)
{
data.button.onPress = function() {
if (data.item == "increase")
switch (data.item)
{
case "raise":
raiseAlert();
return;
case "increase":
increaseAlertLevel();
else if (data.item == "end")
return;
case "end":
endOfAlert();
return;
}
};
if (data.item == "increase")
switch (data.item)
{
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")
case "raise":
data.icon.sprite = "stretched:session/icons/bell_level1.png";
data.button.tooltip = translate("Raise an alert!");
break;
case "increase":
data.icon.sprite = "stretched:session/icons/bell_level2.png";
data.button.tooltip = translate("Increase the alert level to protect more units");
break;
case "end":
data.button.tooltip = translate("End of alert.");
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";
break;
}
data.button.enabled = !data.button.hidden && controlsPlayer(data.unitEntState.player);
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i - 1, data.rowLength);
return true;
}
};
@ -90,9 +99,10 @@ g_SelectionPanels.Barter = {
return 4;
},
"rowLength": 4,
"getItems": function(unitEntState, selection)
"conflictsWith": ["Alert", "Garrison"],
"getItems": function(unitEntStates)
{
if (!unitEntState.barterMarket)
if (unitEntStates.every(state => !state.barterMarket))
return [];
// ["food", "wood", "stone", "metal"]
return BARTER_RESOURCES;
@ -116,7 +126,14 @@ g_SelectionPanels.Barter = {
amountToSell *= BARTER_BUNCH_MULTIPLIER;
amount.Sell.caption = "-" + amountToSell;
let prices = data.unitEntState.barterMarket.prices;
let prices;
for (let state of data.unitEntStates)
if (state.barterMarket)
{
prices = state.barterMarket.prices;
break;
}
amount.Buy.caption = "+" + Math.round(prices.sell[g_BarterSell] / prices.buy[data.item] * amountToSell);
let resource = getLocalizedResourceName(data.item, "withinSentence");
@ -145,7 +162,7 @@ g_SelectionPanels.Barter = {
neededRes[data.item] = amountToSell;
let canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": data.unitEntState.player
"player": data.player
}) ? "color:255 0 0 80:" : "";
// Let's see if we have enough resources to barter.
@ -153,14 +170,14 @@ g_SelectionPanels.Barter = {
neededRes[g_BarterSell] = amountToSell;
let canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": data.unitEntState.player
"player": data.player
}) ? "color:255 0 0 80:" : "";
icon.Sell.sprite = canSellCurrent + "stretched:" + grayscale + "session/icons/resources/" + data.item + ".png";
icon.Buy.sprite = canBuyAny + "stretched:" + grayscale + "session/icons/resources/" + data.item + ".png";
button.Buy.hidden = isSelected;
button.Buy.enabled = controlsPlayer(data.unitEntState.player);
button.Buy.enabled = controlsPlayer(data.player);
button.Sell.hidden = false;
selectionIcon.hidden = !isSelected;
@ -175,18 +192,22 @@ g_SelectionPanels.Command = {
{
return 6;
},
"getItems": function(unitEntState)
"getItems": function(unitEntStates)
{
let commands = [];
for (let c in g_EntityCommands)
for (let command in g_EntityCommands)
{
let info = g_EntityCommands[c].getInfo(unitEntState);
if (!info)
continue;
info.name = c;
commands.push(info);
for (let state of unitEntStates)
{
let info = g_EntityCommands[command].getInfo(state);
if (info)
{
info.name = command;
commands.push(info);
break;
}
}
}
return commands;
},
@ -198,15 +219,15 @@ g_SelectionPanels.Command = {
if (data.item.callback)
data.item.callback(data.item);
else
performCommand(data.unitEntState.id, data.item.name);
performCommand(data.unitEntStates[0], data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled =
g_IsObserver && data.item.name == "focus-rally" ||
controlsPlayer(data.unitEntState.player) &&
(data.item.name != "delete" || !isUndeletable(data.unitEntState));
controlsPlayer(data.player) && (data.item.name != "delete" ||
data.unitEntStates.some(state => !isUndeletable(state)));
data.icon.sprite = "stretched:session/icons/" + data.item.icon;
@ -229,16 +250,21 @@ g_SelectionPanels.AllyCommand = {
return 2;
},
"conflictsWith": ["Command"],
"getItems": function(unitEntState)
"getItems": function(unitEntStates)
{
let commands = [];
for (let c in g_AllyEntityCommands)
for (let command in g_AllyEntityCommands)
{
let info = g_AllyEntityCommands[c].getInfo(unitEntState);
if (!info)
continue;
info.name = c;
commands.push(info);
for (let state of unitEntStates)
{
let info = g_AllyEntityCommands[command].getInfo(state);
if (info)
{
info.name = command;
commands.push(info);
break;
}
}
}
return commands;
},
@ -250,7 +276,7 @@ g_SelectionPanels.AllyCommand = {
if (data.item.callback)
data.item.callback(data.item);
else
performAllyCommand(data.unitEntState.id, data.item.name);
performAllyCommand(data.unitEntStates[0].id, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
@ -292,14 +318,14 @@ g_SelectionPanels.Construction = {
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.unitEntState.player
"player": data.player
});
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, 1),
"player": data.unitEntState.player
"player": data.player
});
if (template.wallSet)
@ -341,7 +367,7 @@ g_SelectionPanels.Construction = {
modifier += resourcesToAlphaMask(neededResources) +":";
}
else
data.button.enabled = controlsPlayer(data.unitEntState.player);
data.button.enabled = controlsPlayer(data.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
@ -359,13 +385,13 @@ g_SelectionPanels.Formation = {
},
"rowLength": 4,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntState)
"getItems": function(unitEntStates)
{
if (!hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal"))
if (unitEntStates.some(state => !hasClass(state, "Unit") || hasClass(state, "Animal")))
return [];
if (!g_AvailableFormations.has(unitEntState.player))
g_AvailableFormations.set(unitEntState.player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntState.player));
return g_AvailableFormations.get(unitEntState.player);
if (!g_AvailableFormations.has(unitEntStates[0].player))
g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player));
return g_AvailableFormations.get(unitEntStates[0].player);
},
"setupButton": function(data)
{
@ -375,18 +401,20 @@ g_SelectionPanels.Formation = {
let formationInfo = g_FormationsInfo.get(data.item);
let formationOk = canMoveSelectionIntoFormation(data.item);
let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": data.selection,
"ents": data.unitEntStates.map(state => state.id),
"formationTemplate": data.item
});
data.button.onPress = function() { performFormation(data.unitEntState.id, data.item); };
data.button.onPress = function() {
performFormation(data.unitEntStates.map(state => state.id), data.item);
};
let tooltip = translate(formationInfo.name);
if (!formationOk && formationInfo.tooltip)
tooltip += "\n" + "[color=\"red\"]" + translate(formationInfo.tooltip) + "[/color]";
data.button.tooltip = tooltip;
data.button.enabled = formationOk && controlsPlayer(data.unitEntState.player);
data.button.enabled = formationOk && controlsPlayer(data.player);
let grayscale = formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !formationSelected;
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon;
@ -402,19 +430,17 @@ g_SelectionPanels.Garrison = {
return 12;
},
"rowLength": 4,
"getItems": function(unitEntState, selection)
"conflictsWith": ["Barter"],
"getItems": function(unitEntStates)
{
if (!unitEntState.garrisonHolder)
if (unitEntStates.every(state => !state.garrisonHolder))
return [];
let groups = new EntityGroups();
for (let ent of selection)
{
let state = GetEntityState(ent);
for (let state of unitEntStates)
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities);
}
return groups.getEntsGrouped();
},
@ -433,7 +459,7 @@ g_SelectionPanels.Garrison = {
let garrisonedUnitOwner = GetEntityState(data.item.ents[0]).player;
let canUngarrison =
g_ViewedPlayer == data.unitEntState.player ||
g_ViewedPlayer == data.player ||
g_ViewedPlayer == garrisonedUnitOwner;
data.button.enabled = canUngarrison && controlsPlayer(g_ViewedPlayer);
@ -470,12 +496,11 @@ g_SelectionPanels.Gate = {
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntState, selection)
"getItems": function(unitEntStates)
{
let gates = [];
for (let ent of selection)
for (let state of unitEntStates)
{
let state = GetEntityState(ent);
if (state.gate && !gates.length)
{
gates.push({
@ -505,7 +530,7 @@ g_SelectionPanels.Gate = {
data.button.tooltip = data.item.tooltip;
data.button.enabled = controlsPlayer(data.unitEntState.player);
data.button.enabled = controlsPlayer(data.player);
let gateIcon;
if (data.item.gate)
{
@ -530,12 +555,11 @@ g_SelectionPanels.Pack = {
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntState, selection)
"getItems": function(unitEntStates)
{
let checks = {};
for (let ent of selection)
for (let state of unitEntStates)
{
let state = GetEntityState(ent);
if (!state.pack)
continue;
@ -603,7 +627,7 @@ g_SelectionPanels.Pack = {
else
data.icon.sprite = "stretched:session/icons/pack.png";
data.button.enabled = controlsPlayer(data.unitEntState.player);
data.button.enabled = controlsPlayer(data.player);
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
@ -616,9 +640,29 @@ g_SelectionPanels.Queue = {
{
return 16;
},
"getItems": function(unitEntState, selection)
/**
* Returns a list of all items in the productionqueue of the selection
* The first entry of every entity's production queue will come before
* the second entry of every entity's production queue
*/
"getItems": function(unitEntStates)
{
return getTrainingQueueItems(selection);
let queue = [];
let foundNew = true;
for (let i = 0; foundNew; ++i)
{
foundNew = false;
for (let state of unitEntStates)
{
if (!state.production || !state.production.queue[i])
continue;
let item = state.production.queue[i];
item.producingEnt = state.id;
queue.push(item);
foundNew = true;
}
}
return queue;
},
"resizePanel": function(numberOfItems, rowLength)
{
@ -671,7 +715,7 @@ g_SelectionPanels.Queue = {
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
data.button.enabled = controlsPlayer(data.unitEntState.player);
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
@ -683,23 +727,37 @@ g_SelectionPanels.Research = {
{
return 8;
},
"getItems": function(unitEntState, selection)
"getItems": function(unitEntStates)
{
// Tech-pairs require two rows. Thus only show techs when there is only one row in use.
// TODO Use a reference instead of a magic number
if (getNumberOfRightPanelButtons() > 8 && selection.length > 1)
return [];
for (let ent of selection)
{
let entState = GetEntityState(ent);
if (entState.production && entState.production.technologies.length)
return entState.production.technologies.map(tech => ({
let ret = [];
if (unitEntStates.length == 1)
return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret :
unitEntStates[0].production.technologies.map(tech => ({
"tech": tech,
"techCostMultiplier": entState.production.techCostMultiplier
"techCostMultiplier": unitEntStates[0].production.techCostMultiplier,
"researchFacilityId": unitEntStates[0].id
}));
for (let state of unitEntStates)
{
if (!state.production || !state.production.technologies)
continue;
// Remove the techs we already have in ret (with the same name and techCostMultiplier)
let filteredTechs = state.production.technologies.filter(
tech => tech != null && !ret.some(
item => item.tech == tech && Object.keys(item.techCostMultiplier).every(
k => item.techCostMultiplier[k] == state.production.techCostMultiplier[k]
)));
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() &&
getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2))
ret = ret.concat(filteredTechs.map(tech => ({
"tech": tech,
"techCostMultiplier": state.production.techCostMultiplier,
"researchFacilityId": state.id
})));
}
return [];
return ret;
},
"hideItem": function(i, rowLength) // Called when no item is found
{
@ -729,7 +787,7 @@ g_SelectionPanels.Research = {
pair.hidden = data.item.tech.pair == null;
setPanelObjectPosition(pair, data.i, data.rowLength);
// Handle one or two techs
// Handle one or two techs (tech pair)
for (let i in techs)
{
let tech = techs[i];
@ -742,12 +800,12 @@ g_SelectionPanels.Research = {
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": template.cost,
"player": data.unitEntState.player
"player": data.player
});
let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
"tech": tech,
"player": data.unitEntState.player
"player": data.player
});
let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]");
@ -764,7 +822,7 @@ g_SelectionPanels.Research = {
let tip = template.requirementsTooltip;
if (template.classRequirements)
{
let player = data.unitEntState.player;
let player = data.player;
let current = GetSimState().players[player].classCounts[template.classRequirements.class] || 0;
let remaining = template.classRequirements.number - current;
tip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), {
@ -777,7 +835,7 @@ g_SelectionPanels.Research = {
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = function () {
addResearchToQueue(data.unitEntState.id, tech);
addResearchToQueue(data.item.researchFacilityId, tech);
};
if (data.item.tech.pair)
@ -806,7 +864,7 @@ g_SelectionPanels.Research = {
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
button.enabled = controlsPlayer(data.unitEntState.player);
button.enabled = controlsPlayer(data.player);
if (template.icon)
icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
@ -827,9 +885,9 @@ g_SelectionPanels.Selection = {
return 16;
},
"rowLength": 4,
"getItems": function(unitEntState, selection)
"getItems": function(unitEntStates)
{
if (selection.length < 2)
if (unitEntStates.length < 2)
return [];
return g_Selection.groups.getTemplateNames();
},
@ -898,25 +956,26 @@ g_SelectionPanels.Stance = {
{
return 5;
},
"getItems": function(unitEntState)
"getItems": function(unitEntStates)
{
if (!unitEntState.unitAI || !hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal"))
if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")))
return [];
return unitEntState.unitAI.possibleStances;
return unitEntStates[0].unitAI.possibleStances;
},
"setupButton": function(data)
{
data.button.onPress = function() { performStance(data.unitEntState, data.item); };
data.button.onPress = function() { performStance(data.unitEntStates.map(state => state.id), data.item); };
data.button.tooltip = getStanceDisplayName(data.item) + "\n" +
"[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]";
data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": data.selection,
"ents": data.unitEntStates.map(state => state.id),
"stance": data.item
});
data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png";
data.button.enabled = controlsPlayer(data.unitEntState.player);
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
@ -940,11 +999,11 @@ g_SelectionPanels.Training = {
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.unitEntState.player
"player": data.player
});
let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingBatchStatus(data.playerState, data.unitEntState.id, data.item, data.selection);
getTrainingBatchStatus(data.playerState, data.item, data.unitEntStates.map(status => status.id));
let trainNum = buildingsCountToTrainFullBatch || 1;
if (Engine.HotkeyIsPressed("session.batchtrain"))
@ -954,10 +1013,12 @@ g_SelectionPanels.Training = {
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, trainNum),
"player": data.unitEntState.player
"player": data.player
});
data.button.onPress = function() { addTrainingToQueue(data.selection, data.item, data.playerState); };
data.button.onPress = function() {
addTrainingToQueue(data.unitEntStates.map(state => state.id), data.item, data.playerState);
};
data.countDisplay.caption = trainNum > 1 ? trainNum : "";
@ -968,7 +1029,7 @@ g_SelectionPanels.Training = {
getVisibleEntityClassesFormatted(template),
getAurasTooltip(template),
getEntityTooltip(template),
getEntityCostTooltip(template, trainNum, data.unitEntState.id)
getEntityCostTooltip(template, trainNum, data.unitEntStates[0].id)
];
let limits = getEntityLimitAndCount(data.playerState, data.item);
@ -1013,7 +1074,7 @@ g_SelectionPanels.Training = {
modifier = resourcesToAlphaMask(neededResources) +":";
}
else
data.button.enabled = controlsPlayer(data.unitEntState.player);
data.button.enabled = controlsPlayer(data.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
@ -1030,14 +1091,13 @@ g_SelectionPanels.Upgrade = {
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntState, selection)
"getItems": function(unitEntStates)
{
// Interface becomes complicated with multiple units and this is meant per-entity, so prevent it if the selection has multiple units.
// TODO: if the units are all the same, this should probably still be possible.
if (selection.length > 1)
// Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units.
if (unitEntStates.some(state => state.template != unitEntStates[0].template))
return false;
return unitEntState.upgrade && unitEntState.upgrade.upgrades;
return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades;
},
"setupButton" : function(data)
{
@ -1050,19 +1110,19 @@ g_SelectionPanels.Upgrade = {
if (data.item.requiredTechnology)
technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": data.item.requiredTechnology,
"player": data.unitEntState.player
"player": data.player
});
let neededResources;
if (data.item.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": data.item.cost,
"player": data.unitEntState.player
"player": data.player
});
let limits = getEntityLimitAndCount(data.playerState, data.item.entity);
let progress = data.unitEntState.upgrade.progress || 0;
let isUpgrading = data.unitEntState.upgrade.template == data.item.entity;
let progress = data.unitEntStates[0].upgrade.progress || 0;
let isUpgrading = data.unitEntStates[0].upgrade.template == data.item.entity;
let tooltip;
if (!progress)

View file

@ -4,8 +4,8 @@
size="4 100%-43 100%-4 100%-4"
type="text"
>
<object size="100%-72 2 100% 100%">
<repeat count="2">
<object size="100%-110 2 100% 100%-2">
<repeat count="3">
<object name="unitAlertButton[n]" hidden="true" style="iconButton" type="button" size="0 0 36 36" tooltip_style="sessionToolTipBottomBold" z="100">
<object name="unitAlertIcon[n]" type="image" ghost="true" size="3 3 33 33"/>
</object>

View file

@ -44,10 +44,10 @@ function setPanelObjectPosition(object, index, rowLength, vMargin = 1, hMargin =
* (i.e. panels with rows of icons) for the currently selected unit.
*
* @param guiName Short identifier string of this panel. See g_SelectionPanels.
* @param unitEntState Entity state of the selected unit with the lowest id.
* @param payerState Player state
* @param unitEntStates Entity states of the selected units
* @param playerState Player state
*/
function setupUnitPanel(guiName, unitEntState, playerState)
function setupUnitPanel(guiName, unitEntStates, playerState)
{
if (!g_SelectionPanels[guiName])
{
@ -56,7 +56,7 @@ function setupUnitPanel(guiName, unitEntState, playerState)
}
let selection = g_Selection.toList();
let items = g_SelectionPanels[guiName].getItems(unitEntState, selection);
let items = g_SelectionPanels[guiName].getItems(unitEntStates);
if (!items || !items.length)
return;
@ -72,9 +72,9 @@ function setupUnitPanel(guiName, unitEntState, playerState)
let data = {
"i": i,
"item": items[i],
"selection": selection,
"playerState": playerState,
"unitEntState": unitEntState,
"player": unitEntStates[0].player,
"unitEntStates": unitEntStates,
"rowLength": rowLength,
"numberOfItems": numberOfItems,
// depending on the XML, some of the GUI objects may be undefined
@ -119,13 +119,12 @@ function setupUnitPanel(guiName, unitEntState, playerState)
* Delegates to setupUnitPanel to set up individual subpanels,
* appropriately activated depending on the selected unit's state.
*
* @param entState Entity state of the selected unit with the lowest id.
* @param entStates Entity states of the selected units
* @param supplementalDetailsPanel Reference to the
* "supplementalSelectionDetails" GUI Object
* @param commandsPanel Reference to the "commandsPanel" GUI Object
* @param selection Array of currently selected entity IDs.
*/
function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection)
function updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel)
{
for (let panel in g_SelectionPanels)
g_SelectionPanels[panel].used = false;
@ -137,7 +136,7 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
let playerStates = GetSimState().players;
let playerState = playerStates[Engine.GetPlayerID()];
if (controlsPlayer(entState.player) || g_IsObserver)
if (g_IsObserver || entStates.every(entState => controlsPlayer(entState.player)))
{
for (var guiName of g_PanelsOrder)
{
@ -145,18 +144,18 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
g_SelectionPanels[guiName].conflictsWith.some(p => g_SelectionPanels[p].used))
continue;
setupUnitPanel(guiName, entState, playerStates[entState.player]);
setupUnitPanel(guiName, entStates, playerStates[entStates[0].player]);
}
supplementalDetailsPanel.hidden = false;
commandsPanel.hidden = false;
}
else if (playerState.isMutualAlly[entState.player]) // owned by allied player
else if (playerState.isMutualAlly[entStates[0].player]) // owned by allied player
{
// TODO if there's a second panel needed for a different player
// we should consider adding the players list to g_SelectionPanels
setupUnitPanel("Garrison", entState, playerState);
setupUnitPanel("AllyCommand", entState, playerState);
setupUnitPanel("Garrison", entStates, playerState);
setupUnitPanel("AllyCommand", entStates, playerState);
supplementalDetailsPanel.hidden = !g_SelectionPanels.Garrison.used;

View file

@ -337,6 +337,11 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
var template = cmpDataTemplateManager.GetTechnologyTemplate(templateName);
if (!template)
return;
if (this.GetTechnologiesList().indexOf(templateName) == -1)
{
warn("This entity cannot research " + JSON.stringify(templateName));
return;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
let techCostMultiplier = this.GetTechCostMultiplier();
let time = techCostMultiplier.time * template.researchTime * cmpPlayer.GetCheatTimeMultiplier();