mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
More versatile placement of research buttons
This patch allows techs to optionally define a property "placeBelow"
specifying a combination of class requirements. The selection panel then
tries to place their research button below the training button whose
unit matches these classes -- if there is only and exactly one.
This is useful for techs only affecting a single type of unit: it e.g.
allows always placing the fishing nets research button below the fishing
boats training button.
To prevent duplicating information in "placeBelow" (e.g. from
"affects"), it can also be set to two magic values: "{AffectedUnit}" and
"{UnlockedUnit}". In practice, many techs use those, but class
combination remain useful for granular control in very specific cases.
Regarding tech pairs, as explained in a comment, their two research
buttons can now be placed in three different arrangements (with
descending preference):
1. Vertically below a single training button.
2. Horizontally below two adjacent training buttons.
3. Horizontally adjacent in the bottom row (below no training buttons).
Vertically in the third and fourth rows (below no training button) --
how it was previously -- is no longer done as it'd conflict with and
doesn't fit the new layout and the clear separation between "generic"
and "specific" techs.
Whenever a research button can't be placed in their preferred location
because it is already occupied, the code simply falls back to the default
position in the bottom row.
This patch also adds a new game option to hide the new small arrows above
the research buttons in order to give more experienced players who already
know the techs well the possibility to reduce visual clutter. By
default, the arrows are shown.
This commit is contained in:
parent
107a49caf1
commit
2a88a41959
52 changed files with 378 additions and 98 deletions
|
|
@ -516,6 +516,7 @@ defaultformation = "special/formations/box" ; For walking orders, automatically
|
|||
formationwalkonly = "true" ; Formations are disabled when giving gather/attack/... orders.
|
||||
howtoshownames = 0 ; Whether the specific names are show as default, as opposed to the generic names. And whether the secondary names are shown. (0 - show both; specific names primary, 1 - show both; generic names primary, 2 - show only specific names, 3 - show only generic names)
|
||||
selectformationasone = "true" ; Whether to select formations as a whole by default.
|
||||
techarrows = true ; Whether to show an arrow above some techs that indicates which unit they affect or unlock.
|
||||
|
||||
[gui.session.minimap]
|
||||
; Icons that are displayed for some entities on a minimap.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0af0db2f9dbdd451452ef7968fef7c73a4d91faf8b9d6663a3f01972ae5364c6
|
||||
size 1588
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cc488cbc99d9c9a16b287cc9fb39fc0e2a25f2046f30a3b39ae6855ae90883bf
|
||||
size 798
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79fcc1b450dc61d7b1df551785a8a116b033e54e41d256adf95c05f66cb1c07e
|
||||
size 798
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d715163e253b20dfeeb4a9509b0a0057d607b27536597e1d3442a939f9c2ee83
|
||||
size 1649
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aeb1f48c5cc648e832e42f474166f3ba6bb052c724d1066ee828d6c8edce0371
|
||||
size 6361
|
||||
oid sha256:11eb2025f1b1db59b1cfe0ed35eea70d0cb0b48eba8f704a09eea2b0be0ca4dc
|
||||
size 6650
|
||||
|
|
|
|||
|
|
@ -592,6 +592,8 @@ function GetTechnologyDataHelper(template, civ, resources)
|
|||
|
||||
ret.tooltip = template.tooltip;
|
||||
ret.requirementsTooltip = template.requirementsTooltip || "";
|
||||
if (template.placeBelow)
|
||||
ret.placeBelow = template.placeBelow;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -640,6 +640,12 @@
|
|||
"tooltip": "Show detailed tooltips for trainable units in structures.",
|
||||
"config": "showdetailedtooltips"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Tech arrows",
|
||||
"tooltip": "Show an arrow above specific techs that points to the unit that they affect or unlock.",
|
||||
"config": "gui.session.techarrows"
|
||||
},
|
||||
{
|
||||
"type": "dropdown",
|
||||
"label": "Naming of entities",
|
||||
|
|
|
|||
|
|
@ -647,8 +647,30 @@ g_SelectionPanels.Research = {
|
|||
return 10;
|
||||
},
|
||||
"rowLength": 10,
|
||||
"init": function()
|
||||
{
|
||||
const updateAffectsIconVisibility = () =>
|
||||
{
|
||||
this.helper.showAffectsIcons = Engine.ConfigDB_GetValue("user", "gui.session.techarrows") === "true";
|
||||
};
|
||||
registerConfigChangeHandler(changes =>
|
||||
{
|
||||
if (changes.has("gui.session.techarrows"))
|
||||
updateAffectsIconVisibility();
|
||||
// They will be rerendered with the new visibility next frame.
|
||||
});
|
||||
updateAffectsIconVisibility();
|
||||
},
|
||||
"reset": function()
|
||||
{
|
||||
this.helper.occupiedPositions = new Set();
|
||||
this.helper.bottomRowButtonCount = 0;
|
||||
},
|
||||
"getItems": function(unitEntStates)
|
||||
{
|
||||
if (getNumberOfRightPanelButtons() >= this.rowLength * 2)
|
||||
return [];
|
||||
|
||||
let ret = [];
|
||||
if (unitEntStates.length == 1)
|
||||
{
|
||||
|
|
@ -690,8 +712,7 @@ g_SelectionPanels.Research = {
|
|||
k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k])
|
||||
));
|
||||
|
||||
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() &&
|
||||
getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2))
|
||||
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems())
|
||||
ret = ret.concat(filteredTechs.map(tech => ({
|
||||
"tech": tech,
|
||||
"techCostMultiplier": state.researcher.techCostMultiplier,
|
||||
|
|
@ -704,77 +725,277 @@ g_SelectionPanels.Research = {
|
|||
"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;
|
||||
// Remove the button it would have been paired with as well.
|
||||
Engine.GetGUIObjectByName("unitResearchButton[" + (i + this.getMaxNumberOfItems()) + "]").hidden = true;
|
||||
},
|
||||
"setupButton": function(data)
|
||||
{
|
||||
if (!data.item.tech)
|
||||
{
|
||||
g_SelectionPanels.Research.hideItem(data.i, data.rowLength);
|
||||
this.hideItem(data.i, data.rowLength);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start position (start at the bottom)
|
||||
let position = data.i + data.rowLength;
|
||||
// There are twice as many button objects than this.getMaxNumberOfItems()
|
||||
// This is because each item could be a tech pair and need a second one in addition to the one at data.i
|
||||
data.j = data.i + this.getMaxNumberOfItems();
|
||||
|
||||
// Only show the top button for pairs
|
||||
if (!data.item.tech.pair)
|
||||
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
|
||||
const playerState = GetSimState().players[data.player];
|
||||
|
||||
// Set up the tech connector
|
||||
const pair = Engine.GetGUIObjectByName("unitResearchPair[" + data.i + "]");
|
||||
pair.hidden = data.item.tech.pair == null;
|
||||
setPanelObjectPosition(pair, data.i, data.rowLength);
|
||||
|
||||
// Handle one or two techs (tech pair)
|
||||
const player = data.player;
|
||||
const playerState = GetSimState().players[player];
|
||||
for (const tech of (data.item.tech.pair || [data.item.tech]))
|
||||
if (data.item.tech.pair)
|
||||
{
|
||||
// Don't change the object returned by GetTechnologyData
|
||||
const template = clone(GetTechnologyData(tech, playerState.civ));
|
||||
if (!template)
|
||||
return false;
|
||||
const firstTemplate = GetTechnologyData(data.item.tech.pair[0], playerState.civ);
|
||||
const secondTemplate = GetTechnologyData(data.item.tech.pair[1], playerState.civ);
|
||||
|
||||
// Not allowed by civ.
|
||||
if (!template.reqs)
|
||||
// template.reqs is false if the tech isn't researchable by the current civ.
|
||||
const firstResearchable = !!firstTemplate?.reqs;
|
||||
const secondResearchable = !!secondTemplate?.reqs;
|
||||
|
||||
if (firstResearchable && secondResearchable)
|
||||
// Ideal/expected case: Display both techs in a pair.
|
||||
return this.helper.setupButtonPair(data, data.item.tech.pair[0], data.item.tech.pair[1], firstTemplate,
|
||||
secondTemplate, playerState);
|
||||
|
||||
// At least one of the two is not valid or researchable. If the other one is, display it as a single tech
|
||||
// on its own.
|
||||
if (firstResearchable && !secondResearchable)
|
||||
return this.helper.setupSingleButton(data, data.item.tech.pair[0], firstTemplate, playerState);
|
||||
if (!firstResearchable && secondResearchable)
|
||||
return this.helper.setupSingleButton(data, data.item.tech.pair[1], secondTemplate, playerState);
|
||||
|
||||
// Neither of the two are valid and researchable.
|
||||
this.hideItem(data.i, data.rowLength);
|
||||
return false;
|
||||
}
|
||||
|
||||
const template = GetTechnologyData(data.item.tech, playerState.civ);
|
||||
// template.reqs is false if the tech isn't researchable by the current civ.
|
||||
if (template?.reqs)
|
||||
return this.helper.setupSingleButton(data, data.item.tech, template, playerState);
|
||||
|
||||
this.hideItem(data.i, data.rowLength);
|
||||
return false;
|
||||
},
|
||||
"helper": {
|
||||
// Techs can optionally define a placeBelow property that specifies a unit whose training button they want to be placed below.
|
||||
// It can be:
|
||||
// - "{UnlockedUnit}": the first unit whose requirements (of the Identity component) contain the tech.
|
||||
// - "{AffectedUnit}": the first unit whose stats are modified (receives buffs or debuffs) by the tech.
|
||||
// - class combination: the first unit whose identity classes match that combination.
|
||||
"findTargetTrainingButton": function(data, techName, template)
|
||||
{
|
||||
// Also check whether the other right panel buttons (training, constructing, upgrading) reach the second row.
|
||||
// In that case, we want to place all techs in the bottom row. Research buttons should never be placed in the
|
||||
// same row as these.
|
||||
if (!template.placeBelow || getNumberOfRightPanelButtons() > data.rowLength)
|
||||
return -1;
|
||||
|
||||
const indices = [];
|
||||
if (template.placeBelow === "{UnlockedUnit}")
|
||||
{
|
||||
// One of the pair may still be researchable by the current civ,
|
||||
// hence don't hide everything.
|
||||
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
|
||||
pair.hidden = true;
|
||||
continue;
|
||||
getAllTrainableEntitiesFromSelection().forEach((trainableTemplate, i) =>
|
||||
{
|
||||
if (GetTemplateData(trainableTemplate, data.player)?.requirements?.Techs?._string.split(/\s+/).includes(techName))
|
||||
indices.push(i);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
let targetClassList;
|
||||
if (template.placeBelow === "{AffectedUnit}")
|
||||
{
|
||||
const affectsList = (template.affects || []);
|
||||
for (const mod of template.modifications)
|
||||
if (mod.affects)
|
||||
affectsList.push(mod.affects);
|
||||
|
||||
targetClassList = affectsList.map(classes => classes.split(/\s+/));
|
||||
}
|
||||
else
|
||||
targetClassList = [template.placeBelow.split(/\s+/)];
|
||||
|
||||
getAllTrainableEntitiesFromSelection().forEach((trainableTemplate, i) =>
|
||||
{
|
||||
if (MatchesClassList(GetTemplateData(trainableTemplate, data.player).visibleIdentityClasses, targetClassList))
|
||||
indices.push(i);
|
||||
});
|
||||
}
|
||||
// Only choose a training button if it's the only matching one.
|
||||
if (indices.length !== 1)
|
||||
return -1;
|
||||
|
||||
// Make sure to account for the other buttons placed before the unit training ones.
|
||||
return indices[0] + ["Construction", "Pack", "Gate", "Upgrade"].reduce((total, panel) =>
|
||||
total + g_unitPanelButtons[panel], 0
|
||||
);
|
||||
|
||||
},
|
||||
"setupSingleButton": function(data, techName, template, playerState)
|
||||
{
|
||||
// The item is not a tech pair. So hide the button that data.button would have been paired with.
|
||||
Engine.GetGUIObjectByName("unitResearchButton[" + data.j + "]").hidden = true;
|
||||
|
||||
// Note: The GUI object container of the research buttons (unlike the one of the training buttons) only reaches up to the second row.
|
||||
// This means that, for example, a research button with position 5 is located directly one row under a training button with position 5.
|
||||
let position = this.findTargetTrainingButton(data, techName, template);
|
||||
|
||||
let placeInBottomRow = position == -1;
|
||||
if (!placeInBottomRow && this.occupiedPositions.has(position))
|
||||
{
|
||||
// Try to fall back to the third (second-to-bottom) row.
|
||||
position += data.rowLength;
|
||||
if (this.occupiedPositions.has(position))
|
||||
// Both positions below the target unit are already used by other techs.
|
||||
// Note: Ideally this should never occur. Two techs per unit should be the limit. This here is just edge case handling.
|
||||
placeInBottomRow = true;
|
||||
}
|
||||
if (placeInBottomRow)
|
||||
{
|
||||
// Try to move it to the fourth (bottom) row.
|
||||
if (this.bottomRowButtonCount >= data.rowLength)
|
||||
return false; // Bottom row is full, we can't display it.
|
||||
position = this.bottomRowButtonCount + data.rowLength * 2;
|
||||
}
|
||||
|
||||
for (const res in template.cost)
|
||||
template.cost[res] *= data.item.techCostMultiplier[res] !== undefined ? data.item.techCostMultiplier[res] : 1;
|
||||
Engine.GetGUIObjectByName("unitResearchVerticalPairIcon[" + data.i + "]").hidden = true;
|
||||
Engine.GetGUIObjectByName("unitResearchHorizontalPairIcon[" + data.i + "]").hidden = true;
|
||||
|
||||
// When it's not "active", it's grayed out.
|
||||
const buttonActive = this.buildButton(data, techName, template, position, playerState, data.button, data.icon);
|
||||
this.buildAffectsIcon(data.i, !placeInBottomRow, buttonActive);
|
||||
|
||||
return true;
|
||||
},
|
||||
"setupButtonPair": function(data, firstTechName, secondTechName, firstTemplate, secondTemplate, playerState)
|
||||
{
|
||||
// Note: The GUI object container of the research buttons (unlike the one of the training buttons) only
|
||||
// reaches up to the second row. This means that, for example, a research button with position 5 is located
|
||||
// directly one row under a training button with position 5.
|
||||
let firstPosition = this.findTargetTrainingButton(data, firstTechName, firstTemplate);
|
||||
let secondPosition = this.findTargetTrainingButton(data, secondTechName, secondTemplate);
|
||||
|
||||
// Possible placements of tech pair with descending preference:
|
||||
// - Vertically below a single unit.
|
||||
// - Horizontally below two adjacent units.
|
||||
// - Horizontally adjacent below no unit in the bottom row.
|
||||
|
||||
// Only ever place either below a unit, if the other can be too and below the same or an adjacent one.
|
||||
let placeInBottomRow = firstPosition == -1 || secondPosition == -1 || Math.abs(firstPosition - secondPosition) > 1;
|
||||
let placeHorizontally = true;
|
||||
if (!placeInBottomRow && firstPosition === secondPosition)
|
||||
{
|
||||
// Both want to be placed under the same unit.
|
||||
// Try to place the pair vertically by moving the second one down to the third (second-to-bottom) row,
|
||||
// below the first one.
|
||||
secondPosition += data.rowLength;
|
||||
if (this.occupiedPositions.has(firstPosition) || this.occupiedPositions.has(secondPosition))
|
||||
placeInBottomRow = true;
|
||||
else
|
||||
placeHorizontally = false;
|
||||
}
|
||||
else if (!placeInBottomRow && (this.occupiedPositions.has(firstPosition) || this.occupiedPositions.has(secondPosition)))
|
||||
{
|
||||
// At least one of the two respective positions in the second (third-to-bottom) row is occupied.
|
||||
// So try move both to the third.
|
||||
firstPosition += data.rowLength;
|
||||
secondPosition += data.rowLength;
|
||||
if (this.occupiedPositions.has(firstPosition) || this.occupiedPositions.has(secondPosition))
|
||||
// Neither the two positions in the second row nor the third row below the target training buttons
|
||||
// are available.
|
||||
placeInBottomRow = true;
|
||||
}
|
||||
|
||||
if (placeInBottomRow)
|
||||
{
|
||||
// Try to move both to the bottom row.
|
||||
if (this.bottomRowButtonCount >= data.rowLength - 1)
|
||||
// Not enough space in the bottom row for both of them. We can't display them.
|
||||
return false;
|
||||
|
||||
firstPosition = this.bottomRowButtonCount + data.rowLength * 2;
|
||||
secondPosition = firstPosition + 1;
|
||||
}
|
||||
|
||||
// Note: the button indices here aren't related to positioning at all.
|
||||
const firstButtonIndex = data.i;
|
||||
const secondButtonIndex = data.j;
|
||||
const firstButton = data.button;
|
||||
const secondButton = Engine.GetGUIObjectByName("unitResearchButton[" + secondButtonIndex + "]");
|
||||
const firstIcon = data.icon;
|
||||
const secondIcon = Engine.GetGUIObjectByName("unitResearchIcon[" + secondButtonIndex + "]");
|
||||
|
||||
// When it's not "active", it's grayed out.
|
||||
const firstButtonActive = this.buildButton(data, firstTechName, firstTemplate, firstPosition, playerState,
|
||||
firstButton, firstIcon);
|
||||
this.buildAffectsIcon(firstButtonIndex, !placeInBottomRow, firstButtonActive);
|
||||
|
||||
// When it's not "active", it's grayed out.
|
||||
const secondButtonActive = this.buildButton(data, secondTechName, secondTemplate, secondPosition, playerState,
|
||||
secondButton, secondIcon);
|
||||
this.buildAffectsIcon(secondButtonIndex, !placeInBottomRow && placeHorizontally, secondButtonActive);
|
||||
|
||||
this.buildPairIcon(false, firstButtonIndex, placeHorizontally && secondPosition > firstPosition, firstButtonActive);
|
||||
this.buildPairIcon(false, secondButtonIndex, placeHorizontally && secondPosition < firstPosition, secondButtonActive);
|
||||
this.buildPairIcon(true, firstButtonIndex, !placeHorizontally, firstButtonActive);
|
||||
this.buildPairIcon(true, secondButtonIndex, false, secondButtonActive);
|
||||
|
||||
// While hovering over either button, show a cross over the other one.
|
||||
// TODO: The following lines have to be executed only once, technically, and not every this function is called.
|
||||
const firstUnchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + firstButtonIndex + "]");
|
||||
const secondUnchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + secondButtonIndex + "]");
|
||||
firstButton.onMouseEnter = () => { secondUnchosenIcon.hidden = false; };
|
||||
firstButton.onMouseLeave = () => { secondUnchosenIcon.hidden = true; };
|
||||
secondButton.onMouseEnter = () => { firstUnchosenIcon.hidden = false; };
|
||||
secondButton.onMouseLeave = () => { firstUnchosenIcon.hidden = true; };
|
||||
|
||||
return true;
|
||||
},
|
||||
"buildAffectsIcon": function(i, show, enable)
|
||||
{
|
||||
const icon = Engine.GetGUIObjectByName("unitResearchAffectsIcon[" + i + "]");
|
||||
icon.hidden = !show || !this.showAffectsIcons;
|
||||
if (!icon.hidden)
|
||||
icon.sprite = "stretched:session/icons/" + (enable ? "tech_affects.png" : "tech_affects_disabled.png");
|
||||
},
|
||||
"buildPairIcon": function(vertical, i, show, enable)
|
||||
{
|
||||
const icon = Engine.GetGUIObjectByName("unitResearch" + (vertical ? "Vertical" : "Horizontal") + "PairIcon[" + i + "]");
|
||||
icon.hidden = !show;
|
||||
if (show)
|
||||
icon.sprite = "stretched:session/icons/" +
|
||||
(vertical ?
|
||||
enable ? "vertical_tech_pair.png" : "vertical_tech_pair_disabled.png" :
|
||||
enable ? "horizontal_tech_pair.png" : "horizontal_tech_pair_disabled.png");
|
||||
},
|
||||
"buildButton": function(baseData, techName, template, position, playerState, button, icon)
|
||||
{
|
||||
// Make sure to not modify the original template.
|
||||
const adaptedTemplate = clone(template);
|
||||
for (const res in adaptedTemplate.cost)
|
||||
adaptedTemplate.cost[res] *=
|
||||
baseData.item.techCostMultiplier[res] !== undefined ? baseData.item.techCostMultiplier[res] : 1;
|
||||
|
||||
const neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
|
||||
"cost": template.cost,
|
||||
"player": player
|
||||
"cost": adaptedTemplate.cost,
|
||||
"player": baseData.player
|
||||
});
|
||||
|
||||
const requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
|
||||
"tech": tech,
|
||||
"player": player
|
||||
"tech": techName,
|
||||
"player": baseData.player
|
||||
});
|
||||
|
||||
const button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]");
|
||||
const icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]");
|
||||
|
||||
const tooltips = [
|
||||
getEntityNamesFormatted,
|
||||
getEntityTooltip,
|
||||
getEntityCostTooltip,
|
||||
getTemplateViewerOnRightClickTooltip
|
||||
].map(func => func(template));
|
||||
].map(func => func(adaptedTemplate));
|
||||
|
||||
if (!requirementsPassed)
|
||||
{
|
||||
let tip = template.requirementsTooltip;
|
||||
const reqs = template.reqs;
|
||||
let tip = adaptedTemplate.requirementsTooltip;
|
||||
const reqs = adaptedTemplate.reqs;
|
||||
for (const req of reqs)
|
||||
{
|
||||
if (!req.entities)
|
||||
|
|
@ -819,39 +1040,27 @@ g_SelectionPanels.Research = {
|
|||
|
||||
button.onPress = (t => function()
|
||||
{
|
||||
addResearchToQueue(data.item.researchFacilityId, t);
|
||||
})(tech);
|
||||
addResearchToQueue(baseData.item.researchFacilityId, t);
|
||||
})(techName);
|
||||
|
||||
const showTemplateFunc = (t => function()
|
||||
{
|
||||
showTemplateDetails(
|
||||
t,
|
||||
GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv);
|
||||
GetTemplateData(baseData.unitEntStates.find(state => state.id == baseData.item.researchFacilityId).template).nativeCiv);
|
||||
});
|
||||
|
||||
button.onPressRight = showTemplateFunc(tech);
|
||||
button.onPressRightDisabled = showTemplateFunc(tech);
|
||||
|
||||
if (data.item.tech.pair)
|
||||
{
|
||||
// On mouse enter, show a cross over the other icon
|
||||
const unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]");
|
||||
button.onMouseEnter = function()
|
||||
{
|
||||
unchosenIcon.hidden = false;
|
||||
};
|
||||
button.onMouseLeave = function()
|
||||
{
|
||||
unchosenIcon.hidden = true;
|
||||
};
|
||||
}
|
||||
button.onPressRight = showTemplateFunc(techName);
|
||||
button.onPressRightDisabled = showTemplateFunc(techName);
|
||||
|
||||
button.hidden = false;
|
||||
let modifier = "";
|
||||
let isActive = true;
|
||||
if (!requirementsPassed)
|
||||
{
|
||||
button.enabled = false;
|
||||
modifier += "color:0 0 0 127:grayscale:";
|
||||
isActive = false;
|
||||
}
|
||||
else if (neededResources)
|
||||
{
|
||||
|
|
@ -859,26 +1068,32 @@ g_SelectionPanels.Research = {
|
|||
modifier += resourcesToAlphaMask(neededResources) + ":";
|
||||
}
|
||||
else
|
||||
button.enabled = controlsPlayer(data.player);
|
||||
button.enabled = controlsPlayer(baseData.player);
|
||||
|
||||
if (data.item.isUpgrading)
|
||||
if (baseData.item.isUpgrading)
|
||||
{
|
||||
button.enabled = false;
|
||||
modifier += "color:0 0 0 127:grayscale:";
|
||||
isActive = false;
|
||||
button.tooltip += "\n" + objectionFont(translate("Cannot research while upgrading."));
|
||||
|
||||
}
|
||||
|
||||
if (template.icon)
|
||||
icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
|
||||
if (adaptedTemplate.icon)
|
||||
icon.sprite = modifier + "stretched:session/portraits/" + adaptedTemplate.icon;
|
||||
|
||||
setPanelObjectPosition(button, position, data.rowLength);
|
||||
this.occupiedPositions.add(position);
|
||||
if (position >= 2 * baseData.rowLength)
|
||||
this.bottomRowButtonCount++;
|
||||
|
||||
// Prepare to handle the top button (if any)
|
||||
position -= data.rowLength;
|
||||
// The panel is a bit higher than 4 * baseData.rowLength, which allows us to visibility anchor the buttons
|
||||
// in the bottom row to the bottom by moving them down those few pixels. Else the gap would be at the bottom.
|
||||
// This creates a small spatial separation between the "generic" techs in the bottom row and the "specific"
|
||||
// techs above them.
|
||||
const vOffset = position >= baseData.rowLength * 2 ? 6 : 0;
|
||||
setPanelObjectPosition(button, position, baseData.rowLength, 1, 1, vOffset);
|
||||
|
||||
return isActive;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1261,10 +1476,12 @@ g_SelectionPanels.Upgrade = {
|
|||
|
||||
function initSelectionPanels()
|
||||
{
|
||||
|
||||
const unitBarterPanel = Engine.GetGUIObjectByName("unitBarterPanel");
|
||||
if (BarterButtonManager.IsAvailable(unitBarterPanel))
|
||||
g_SelectionPanelBarterButtonManager = new BarterButtonManager(unitBarterPanel);
|
||||
|
||||
for (const panel in g_SelectionPanels)
|
||||
g_SelectionPanels[panel].init?.();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<object name="unitResearchPanel"
|
||||
size="6 100%-82 100% 100%"
|
||||
size="6 45 100% 100%"
|
||||
>
|
||||
<object>
|
||||
<repeat count="20">
|
||||
<object name="unitResearchButton[n]" hidden="true" style="iconButton" type="button" size="0 0 38 38" tooltip_style="sessionToolTipBottom">
|
||||
<object name="unitResearchIcon[n]" type="image" ghost="true" size="3 3 35 35"/>
|
||||
<object name="unitResearchUnchosenIcon[n]" type="image" hidden="true" ghost="true" size="3 3 35 35" sprite="stretched:session/icons/tech_pair_would_be_unavailable.png"/>
|
||||
</object>
|
||||
</repeat>
|
||||
<repeat count="10">
|
||||
<object name="unitResearchPair[n]" hidden="true" size="0 0 38 76">
|
||||
<object name="unitResearchPairIcon[n]" type="image" ghost="true" size="8 30 30 46" sprite="stretched:session/icons/vertical_pair.png"/>
|
||||
<object name="unitResearchAffectsIcon[n]" type="image" z="60" hidden="true" ghost="true" size="4 -10 34 6"/>
|
||||
<!-- Positioned for the case that the second tech in the pair is located below it. -->
|
||||
<object name="unitResearchVerticalPairIcon[n]" type="image" z="60" ghost="true" hidden="true" size="8 30 30 46"/>
|
||||
<!-- Positioned for the case that the second tech in the pair is located to the right of it. -->
|
||||
<object name="unitResearchHorizontalPairIcon[n]" type="image" z="60" ghost="true" hidden="true" size="30 8 46 30"/>
|
||||
</object>
|
||||
</repeat>
|
||||
</object>
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
|
||||
<!-- Commands Panel (right). -->
|
||||
<object name="unitCommands"
|
||||
size="50%+110 100%-166 50%+512 100%"
|
||||
size="50%+110 100%-173 50%+512 100%"
|
||||
sprite="unitCommandsPanel"
|
||||
type="image"
|
||||
z="20"
|
||||
|
|
|
|||
|
|
@ -550,6 +550,11 @@
|
|||
<image backcolor="0 0 0 185"/>
|
||||
</sprite>
|
||||
|
||||
<sprite name="ResearchPanelSeparator">
|
||||
<image backcolor="57 55 30"/>
|
||||
<image backcolor="89 77 23" size="0 1 100% 2"/>
|
||||
</sprite>
|
||||
|
||||
<!-- ================================ ================================ -->
|
||||
<!-- Unit portrait -->
|
||||
<!-- ================================ ================================ -->
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ var g_unitPanelButtons = {
|
|||
* Will wrap around to subsequent rows if the index
|
||||
* is larger than rowLength.
|
||||
*/
|
||||
function setPanelObjectPosition(object, index, rowLength, vMargin = 1, hMargin = 1)
|
||||
function setPanelObjectPosition(object, index, rowLength, vMargin = 1, hMargin = 1, vOffset = 0, hOffset = 0)
|
||||
{
|
||||
const oWidth = object.size.right - object.size.left;
|
||||
const oHeight = object.size.bottom - object.size.top;
|
||||
const left = (index % rowLength) * (oWidth + vMargin);
|
||||
const top = (Math.floor(index / rowLength)) * (oHeight + hMargin);
|
||||
const left = (index % rowLength) * (oWidth + hMargin) + hOffset;
|
||||
const top = (Math.floor(index / rowLength)) * (oHeight + vMargin) + vOffset;
|
||||
|
||||
Object.assign(object.size, {
|
||||
// horizontal position
|
||||
|
|
@ -55,15 +55,11 @@ function setupUnitPanel(guiName, unitEntStates, playerState)
|
|||
return;
|
||||
}
|
||||
|
||||
const items = g_SelectionPanels[guiName].getItems(unitEntStates);
|
||||
|
||||
if (!items || !items.length)
|
||||
return;
|
||||
|
||||
const items = g_SelectionPanels[guiName].getItems(unitEntStates) || [];
|
||||
const numberOfItems = Math.min(items.length, g_SelectionPanels[guiName].getMaxNumberOfItems());
|
||||
const rowLength = g_SelectionPanels[guiName].rowLength || 8;
|
||||
|
||||
if (g_SelectionPanels[guiName].resizePanel)
|
||||
if (numberOfItems && g_SelectionPanels[guiName].resizePanel)
|
||||
g_SelectionPanels[guiName].resizePanel(numberOfItems, rowLength);
|
||||
|
||||
for (let i = 0; i < numberOfItems; ++i)
|
||||
|
|
@ -105,10 +101,13 @@ function setupUnitPanel(guiName, unitEntStates, playerState)
|
|||
if (g_SelectionPanels[guiName].hideItem)
|
||||
g_SelectionPanels[guiName].hideItem(i, rowLength);
|
||||
else
|
||||
Engine.GetGUIObjectByName("unit" + guiName + "Button[" + i + "]").hidden = true;
|
||||
{
|
||||
const button = Engine.TryGetGUIObjectByName("unit" + guiName + "Button[" + i + "]");
|
||||
if (button) button.hidden = true;
|
||||
}
|
||||
|
||||
g_unitPanelButtons[guiName] = numberOfItems;
|
||||
g_SelectionPanels[guiName].used = true;
|
||||
g_SelectionPanels[guiName].used = numberOfItems > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -126,7 +125,10 @@ function setupUnitPanel(guiName, unitEntStates, playerState)
|
|||
function updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel)
|
||||
{
|
||||
for (const panel in g_SelectionPanels)
|
||||
{
|
||||
g_SelectionPanels[panel].used = false;
|
||||
g_SelectionPanels[panel].reset?.();
|
||||
}
|
||||
|
||||
// Get player state to check some constraints
|
||||
// e.g. presence of a hero or build limits.
|
||||
|
|
@ -140,11 +142,9 @@ function updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel)
|
|||
{
|
||||
for (const guiName of g_PanelsOrder)
|
||||
{
|
||||
if (g_SelectionPanels[guiName].conflictsWith &&
|
||||
g_SelectionPanels[guiName].conflictsWith.some(p => g_SelectionPanels[p].used))
|
||||
continue;
|
||||
|
||||
setupUnitPanel(guiName, entStates, playerStates[entStates[0].player]);
|
||||
if (!g_SelectionPanels[guiName].conflictsWith ||
|
||||
g_SelectionPanels[guiName].conflictsWith.every(p => !g_SelectionPanels[p].used))
|
||||
setupUnitPanel(guiName, entStates, playerStates[entStates[0].player]);
|
||||
}
|
||||
|
||||
supplementalDetailsPanel.hidden = false;
|
||||
|
|
|
|||
|
|
@ -21,5 +21,6 @@
|
|||
{ "value": "Health/Max", "multiply": 1.25 }
|
||||
],
|
||||
"affects": ["Champion Infantry Spearman"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,6 @@
|
|||
{ "value": "Loot/food", "add": 5 }
|
||||
],
|
||||
"affects": ["Mercenary Swordsman"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,6 @@
|
|||
{ "value": "Cost/BuildTime", "multiply": 0.5 }
|
||||
],
|
||||
"affects": ["Minister"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,6 @@
|
|||
{ "value": "Health/Max", "multiply": 1.5 }
|
||||
],
|
||||
"affects": ["Minister"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,6 @@
|
|||
{ "value": "Loot/metal", "replace": 0 }
|
||||
],
|
||||
"affects": ["Healer !Champion !Hero"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@
|
|||
{ "value": "Cost/BuildTime", "multiply": 0.8 }
|
||||
],
|
||||
"affects": ["Crossbowman"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@
|
|||
{ "value": "ResourceGatherer/Capacities/food", "add": 20 }
|
||||
],
|
||||
"affects": ["FishingBoat"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@
|
|||
{ "value": "ResourceGatherer/Rates/food.fish", "multiply": 1.3 }
|
||||
],
|
||||
"affects": ["FishingBoat"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@
|
|||
"modifications": [
|
||||
{ "value": "Health/Max", "multiply": 1.5, "affects": "Healer" }
|
||||
],
|
||||
"placeBelow": "Champion Healer",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@
|
|||
{ "value": "Vision/Range", "add": 5 }
|
||||
],
|
||||
"affects": ["Healer"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@
|
|||
{ "value": "Vision/Range", "add": 5 }
|
||||
],
|
||||
"affects": ["Healer"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,6 @@
|
|||
{ "value": "Heal/Interval", "multiply": 0.8 }
|
||||
],
|
||||
"affects": ["Healer"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@
|
|||
{ "value": "Heal/Interval", "multiply": 0.8 }
|
||||
],
|
||||
"affects": ["Healer"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@
|
|||
{ "value": "Health/Max", "multiply": 2 }
|
||||
],
|
||||
"affects": ["Civilian"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@
|
|||
{ "value": "ResourceGatherer/Rates/food.grain", "multiply": 2 }
|
||||
],
|
||||
"affects": ["Infantry Javelineer"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,6 @@
|
|||
{ "value": "Health/Max", "multiply": 1.1 }
|
||||
],
|
||||
"affects": ["Infantry Spearman !Hero"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@
|
|||
{ "value": "Cost/BuildTime", "multiply": 0.5 }
|
||||
],
|
||||
"affects": ["Immortal"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,6 @@
|
|||
{ "value": "Attack/Melee/Damage/Pierce", "multiply": 1.1, "affects": "Champion" },
|
||||
{ "value": "Cost/BuildTime", "multiply": 1.3, "affects": "Citizen Infantry Javelineer" }
|
||||
],
|
||||
"placeBelow": "Champion Spearman",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,5 +29,6 @@
|
|||
{ "value": "Health/Max", "multiply": 1.1 }
|
||||
],
|
||||
"affects": ["Champion Cavalry Spearman"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@
|
|||
"requirementsTooltip": "Unlocked in City Phase.",
|
||||
"icon": "helmet_corinthian_crest.png",
|
||||
"tooltip": "Unlock the Champion Infantry Swordsman.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,6 @@
|
|||
{ "value": "Attack/Ranged/Projectile/Spread", "multiply": 0.8 }
|
||||
],
|
||||
"affects": ["BoltShooter"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@
|
|||
{ "value": "Promotion/RequiredXp", "replace": 0 }
|
||||
],
|
||||
"affects": ["Champion Infantry"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@
|
|||
{ "value": "Health/Max", "multiply": 1.5 }
|
||||
],
|
||||
"affects": ["Trader !Ship"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@
|
|||
"requirementsTooltip": "Unlocked in City Phase.",
|
||||
"icon": "helmet_corinthian_crest.png",
|
||||
"tooltip": "Unlock the Champion Infantry Pikeman.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@
|
|||
{ "value": "UnitMotion/WalkSpeed", "multiply": 1.1 }
|
||||
],
|
||||
"affects": ["Champion Melee Infantry !Hero"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,6 @@
|
|||
"icon": "helmet_corinthian_crest.png",
|
||||
"researchTime": 60,
|
||||
"tooltip": "Unlock Champion Cavalry at the Stable.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,6 @@
|
|||
"icon": "helmet_corinthian_crest.png",
|
||||
"researchTime": 60,
|
||||
"tooltip": "Unlock Champion Chariots at the Stable.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@
|
|||
"icon": "helmet_corinthian_crest.png",
|
||||
"researchTime": 60,
|
||||
"tooltip": "Unlock Champions Infantry at the Barracks.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@
|
|||
"icon": "wives_festival.png",
|
||||
"researchTime": 60,
|
||||
"tooltip": "Unlock the ability to train Civilians from houses.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
"icon": "wives_festival_african.png",
|
||||
"researchTime": 60,
|
||||
"tooltip": "Unlock the ability to train Civilians from houses.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,6 @@
|
|||
"icon": "wives_festival.png",
|
||||
"researchTime": 40,
|
||||
"tooltip": "Unlock the ability to train women from houses.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,5 +14,6 @@
|
|||
"icon": "helmet_corinthian_bronze_old.png",
|
||||
"researchTime": 60,
|
||||
"tooltip": "Unlock the ability to train Spearman Neodamodes at the Barracks.",
|
||||
"placeBelow": "{UnlockedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@
|
|||
{ "value": "Promotion/RequiredXp", "replace": 0, "affects": "Wagon" },
|
||||
{ "value": "Population/Bonus", "add": 10, "affects": "Colony" }
|
||||
],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@
|
|||
{ "value": "Attack/Ranged/MaxRange", "multiply": 1.1 }
|
||||
],
|
||||
"affects": ["ArrowShip"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,5 +30,6 @@
|
|||
{ "value": "Health/Max", "multiply": 1.25 }
|
||||
],
|
||||
"affects": ["Ignited"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,6 @@
|
|||
{ "value": "Attack/Melee/Damage/Hack", "multiply": 1.3 }
|
||||
],
|
||||
"affects": ["NavalRam"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,5 +30,6 @@
|
|||
{ "value": "Attack/Ranged/MaxRange", "multiply": 1.2 }
|
||||
],
|
||||
"affects": ["NavalSiege"],
|
||||
"placeBelow": "{AffectedUnit}",
|
||||
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue