Index: ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 20874)
+++ ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 20875)
@@ -1,126 +1,126 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 20874)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 20875)
@@ -1,553 +1,553 @@
function layoutSelectionSingle()
{
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
function layoutSelectionMultiple()
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
function getResourceTypeDisplayName(resourceType)
{
return resourceNameFirstWord(
resourceType.generic == "treasure" ?
resourceType.specific :
resourceType.generic);
}
// Updates the health bar of garrisoned units
function updateGarrisonHealthBar(entState, selection)
{
if (!entState.garrisonHolder)
return;
// Summing up the Health of every single unit
let totalGarrisonHealth = 0;
let maxGarrisonHealth = 0;
for (let selEnt of selection)
{
let selEntState = GetEntityState(selEnt);
if (selEntState.garrisonHolder)
for (let ent of selEntState.garrisonHolder.entities)
{
let state = GetEntityState(ent);
totalGarrisonHealth += state.hitpoints || 0;
maxGarrisonHealth += state.maxHitpoints || 0;
}
}
// Configuring the health bar
let healthGarrison = Engine.GetGUIObjectByName("healthGarrison");
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, totalGarrisonHealth / maxGarrisonHealth));
healthBarGarrison.size = healthSize;
healthGarrison.tooltip = getCurrentHealthTooltip({
"hitpoints": totalGarrisonHealth,
"maxHitpoints": maxGarrisonHealth
});
}
}
// Fills out information that most entities have
function displaySingle(entState)
{
// Get general unit and player data
let template = GetTemplateData(entState.template);
let specificName = template.name.specific;
let genericName = template.name.generic;
// If packed, add that to the generic name (reduces template clutter)
if (genericName && template.pack && template.pack.state == "packed")
genericName = sprintf(translate("%(genericName)s — Packed"), { "genericName": genericName });
let playerState = g_Players[entState.player];
let civName = g_CivData[playerState.civ].Name;
let civEmblem = g_CivData[playerState.civ].Emblem;
let playerName = playerState.name;
let playerColor = playerState.color.r + " " + playerState.color.g + " " + playerState.color.b + " 128";
// Indicate disconnected players by prefixing their name
if (g_Players[entState.player].offline)
playerName = sprintf(translate("\\[OFFLINE] %(player)s"), { "player": playerName });
// Rank
if (entState.identity && entState.identity.rank && entState.identity.classes)
{
Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), {
"rank": translateWithContext("Rank", entState.identity.rank)
});
Engine.GetGUIObjectByName("rankIcon").sprite = getRankIconSprite(entState);
Engine.GetGUIObjectByName("rankIcon").hidden = false;
}
else
{
Engine.GetGUIObjectByName("rankIcon").hidden = true;
Engine.GetGUIObjectByName("rankIcon").tooltip = "";
}
let showHealth = entState.hitpoints;
let showResource = entState.resourceSupply;
let healthSection = Engine.GetGUIObjectByName("healthSection");
let captureSection = Engine.GetGUIObjectByName("captureSection");
let resourceSection = Engine.GetGUIObjectByName("resourceSection");
let sectionPosTop = Engine.GetGUIObjectByName("sectionPosTop");
let sectionPosMiddle = Engine.GetGUIObjectByName("sectionPosMiddle");
let sectionPosBottom = Engine.GetGUIObjectByName("sectionPosBottom");
// Hitpoints
healthSection.hidden = !showHealth;
if (showHealth)
{
let unitHealthBar = Engine.GetGUIObjectByName("healthBar");
let healthSize = unitHealthBar.size;
healthSize.rright = 100 * Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints));
unitHealthBar.size = healthSize;
if (entState.foundation && entState.visibility == "visible" && entState.foundation.numBuilders !== 0 && entState.buildTime)
Engine.GetGUIObjectByName("health").tooltip = sprintf(
translatePlural(
"This foundation will be completed in %(seconds)s second.",
"This foundation will be completed in %(seconds)s seconds.",
Math.ceil(entState.buildTime.timeRemaining)),
{
"seconds": Math.ceil(entState.buildTime.timeRemaining)
});
else
Engine.GetGUIObjectByName("health").tooltip = "";
Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), {
"hitpoints": Math.ceil(entState.hitpoints),
"maxHitpoints": Math.ceil(entState.maxHitpoints)
});
healthSection.size = sectionPosTop.size;
captureSection.size = showResource ? sectionPosMiddle.size : sectionPosBottom.size;
resourceSection.size = showResource ? sectionPosBottom.size : sectionPosMiddle.size;
}
else
{
captureSection.size = sectionPosBottom.size;
resourceSection.size = sectionPosTop.size;
}
// CapturePoints
captureSection.hidden = !entState.capturePoints;
if (entState.capturePoints)
{
let setCaptureBarPart = function(playerID, startSize) {
let unitCaptureBar = Engine.GetGUIObjectByName("captureBar[" + playerID + "]");
let sizeObj = unitCaptureBar.size;
sizeObj.rleft = startSize;
let size = 100 * Math.max(0, Math.min(1, entState.capturePoints[playerID] / entState.maxCapturePoints));
sizeObj.rright = startSize + size;
unitCaptureBar.size = sizeObj;
unitCaptureBar.sprite = "color: " + rgbToGuiColor(g_Players[playerID].color, 128);
unitCaptureBar.hidden = false;
return startSize + size;
};
// first handle the owner's points, to keep those points on the left for clarity
let size = setCaptureBarPart(entState.player, 0);
for (let i in entState.capturePoints)
if (i != entState.player)
size = setCaptureBarPart(i, size);
let captureText = sprintf(translate("%(capturePoints)s / %(maxCapturePoints)s"), {
"capturePoints": Math.ceil(entState.capturePoints[entState.player]),
"maxCapturePoints": Math.ceil(entState.maxCapturePoints)
});
let showSmallCapture = showResource && showHealth;
Engine.GetGUIObjectByName("captureStats").caption = showSmallCapture ? "" : captureText;
Engine.GetGUIObjectByName("capture").tooltip = showSmallCapture ? captureText : "";
}
// Experience
Engine.GetGUIObjectByName("experience").hidden = !entState.promotion;
if (entState.promotion)
{
let experienceBar = Engine.GetGUIObjectByName("experienceBar");
let experienceSize = experienceBar.size;
experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req)));
experienceBar.size = experienceSize;
if (entState.promotion.curr < entState.promotion.req)
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s / %(required)s"), {
"experience": "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
"current": Math.floor(entState.promotion.curr),
"required": entState.promotion.req
});
else
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s"), {
"experience": "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
"current": Math.floor(entState.promotion.curr)
});
}
// Resource stats
resourceSection.hidden = !showResource;
if (entState.resourceSupply)
{
let resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol
sprintf(translate("%(amount)s / %(max)s"), {
"amount": Math.ceil(+entState.resourceSupply.amount),
"max": entState.resourceSupply.max
});
let unitResourceBar = Engine.GetGUIObjectByName("resourceBar");
let resourceSize = unitResourceBar.size;
resourceSize.rright = entState.resourceSupply.isInfinite ? 100 :
100 * Math.max(0, Math.min(1, +entState.resourceSupply.amount / +entState.resourceSupply.max));
unitResourceBar.size = resourceSize;
Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), {
"resource": getResourceTypeDisplayName(entState.resourceSupply.type)
});
Engine.GetGUIObjectByName("resourceStats").caption = resources;
}
// Resource carrying
if (entState.resourceCarrying && entState.resourceCarrying.length)
{
// We should only be carrying one resource type at once, so just display the first
let carried = entState.resourceCarrying[0];
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/" + carried.type + ".png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { "amount": carried.amount, "max": carried.max });
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = "";
}
// Use the same indicators for traders
else if (entState.trader && entState.trader.goods.amount)
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/" + entState.trader.goods.type + ".png";
let totalGain = entState.trader.goods.amount.traderGain;
if (entState.trader.goods.amount.market1Gain)
totalGain += entState.trader.goods.amount.market1Gain;
if (entState.trader.goods.amount.market2Gain)
totalGain += entState.trader.goods.amount.market2Gain;
Engine.GetGUIObjectByName("resourceCarryingText").caption = totalGain;
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(entState.trader.goods.amount)
});
}
// And for number of workers
else if (entState.foundation && entState.visibility == "visible")
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.foundation.numBuilders + " ";
if (entState.foundation.numBuilders !== 0 && entState.buildTime)
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(
translatePlural(
"Number of builders.\nTasking another to this foundation would speed construction up by %(speedup)s second.",
"Number of builders.\nTasking another to this foundation would speed construction up by %(speedup)s seconds.",
Math.ceil(entState.buildTime.timeSpeedup)),
{
"speedup": Math.ceil(entState.buildTime.timeSpeedup)
});
else
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Number of builders.");
}
else if (entState.repairable && entState.repairable.numBuilders > 0 && entState.visibility == "visible")
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.repairable.numBuilders + " ";
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Number of builders.");
}
else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints) && entState.visibility == "visible")
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), {
"amount": entState.resourceSupply.numGatherers,
"max": entState.resourceSupply.maxGatherers
}) + " ";
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Current/max gatherers");
}
else
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = true;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = true;
}
Engine.GetGUIObjectByName("specific").caption = specificName;
Engine.GetGUIObjectByName("player").caption = playerName;
Engine.GetGUIObjectByName("playerColorBackground").sprite = "color: " + playerColor;
Engine.GetGUIObjectByName("generic").caption = genericName == specificName ? "" :
sprintf(translate("(%(genericName)s)"), {
"genericName": genericName
});
let isGaia = playerState.civ == "gaia";
Engine.GetGUIObjectByName("playerCivIcon").sprite = isGaia ? "" : "stretched:grayscale:" + civEmblem;
Engine.GetGUIObjectByName("player").tooltip = isGaia ? "" : civName;
// TODO: we should require all entities to have icons
Engine.GetGUIObjectByName("icon").sprite = template.icon ? ("stretched:session/portraits/" + template.icon) : "BackgroundBlack";
Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = [
getAttackTooltip,
getSplashDamageTooltip,
getHealerTooltip,
getArmorTooltip,
getGatherTooltip,
getRepairRateTooltip,
getBuildRateTooltip,
getSpeedTooltip,
getGarrisonTooltip,
getProjectilesTooltip,
getResourceTrickleTooltip,
getLootTooltip
].map(func => func(entState)).filter(tip => tip).join("\n");
let iconTooltips = [];
if (genericName)
iconTooltips.push("[font=\"sans-bold-16\"]" + genericName + "[/font]");
iconTooltips = iconTooltips.concat([
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip
].map(func => func(template)));
Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltips.filter(tip => tip).join("\n");
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
// Fills out information for multiple entities
function displayMultiple(entStates)
{
let averageHealth = 0;
let maxHealth = 0;
let maxCapturePoints = 0;
let capturePoints = (new Array(g_MaxPlayers + 1)).fill(0);
let playerID = 0;
let totalCarrying = {};
let totalLoot = {};
for (let entState of entStates)
{
playerID = entState.player; // trust that all selected entities have the same owner
if (entState.hitpoints)
{
averageHealth += entState.hitpoints;
maxHealth += entState.maxHitpoints;
}
if (entState.capturePoints)
{
maxCapturePoints += entState.maxCapturePoints;
capturePoints = entState.capturePoints.map((v, i) => v + capturePoints[i]);
}
let carrying = calculateCarriedResources(
entState.resourceCarrying,
entState.trader && entState.trader.goods
);
for (let type in entState.loot)
totalLoot[type] = (totalLoot[type] || 0) + entState.loot[type];
for (let type in carrying)
{
totalCarrying[type] = (totalCarrying[type] || 0) + carrying[type];
totalLoot[type] = (totalLoot[type] || 0) + carrying[type];
}
}
Engine.GetGUIObjectByName("healthMultiple").hidden = averageHealth <= 0;
if (averageHealth > 0)
{
let unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple");
let healthSize = unitHealthBar.size;
healthSize.rtop = 100 - 100 * Math.max(0, Math.min(1, averageHealth / maxHealth));
unitHealthBar.size = healthSize;
Engine.GetGUIObjectByName("healthMultiple").tooltip = getCurrentHealthTooltip({
"hitpoints": averageHealth,
"maxHitpoints": maxHealth
});
}
Engine.GetGUIObjectByName("captureMultiple").hidden = maxCapturePoints <= 0;
if (maxCapturePoints > 0)
{
let setCaptureBarPart = function(pID, startSize)
{
let unitCaptureBar = Engine.GetGUIObjectByName("captureBarMultiple[" + pID + "]");
let sizeObj = unitCaptureBar.size;
sizeObj.rtop = startSize;
let size = 100 * Math.max(0, Math.min(1, capturePoints[pID] / maxCapturePoints));
sizeObj.rbottom = startSize + size;
unitCaptureBar.size = sizeObj;
unitCaptureBar.sprite = "color: " + rgbToGuiColor(g_Players[pID].color, 128);
unitCaptureBar.hidden = false;
return startSize + size;
};
let size = 0;
for (let i in capturePoints)
if (i != playerID)
size = setCaptureBarPart(i, size);
// last handle the owner's points, to keep those points on the bottom for clarity
setCaptureBarPart(playerID, size);
Engine.GetGUIObjectByName("captureMultiple").tooltip = getCurrentHealthTooltip(
{
"hitpoints": capturePoints[playerID],
"maxHitpoints": maxCapturePoints
},
translate("Capture Points:"));
}
let numberOfUnits = Engine.GetGUIObjectByName("numberOfUnits");
numberOfUnits.caption = entStates.length;
numberOfUnits.tooltip = "";
if (Object.keys(totalCarrying).length)
numberOfUnits.tooltip = sprintf(translate("%(label)s %(details)s\n"), {
"label": headerFont(translate("Carrying:")),
"details": bodyFont(Object.keys(totalCarrying).filter(
res => totalCarrying[res] != 0).map(
res => sprintf(translate("%(type)s %(amount)s"),
{ "type": resourceIcon(res), "amount": totalCarrying[res] })).join(" "))
});
if (Object.keys(totalLoot).length)
numberOfUnits.tooltip += sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Loot:")),
"details": bodyFont(Object.keys(totalLoot).filter(
res => totalLoot[res] != 0).map(
res => sprintf(translate("%(type)s %(amount)s"),
{ "type": resourceIcon(res), "amount": totalLoot[res] })).join(" "))
});
// Unhide Details Area
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
// Updates middle entity Selection Details Panel and left Unit Commands Panel
function updateSelectionDetails()
{
let supplementalDetailsPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails");
let detailsPanel = Engine.GetGUIObjectByName("selectionDetails");
let commandsPanel = Engine.GetGUIObjectByName("unitCommands");
let entStates = [];
for (let sel of g_Selection.toList())
{
- let entState = GetExtendedEntityState(sel);
+ let entState = GetEntityState(sel);
if (!entState)
continue;
entStates.push(entState);
}
if (entStates.length == 0)
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
hideUnitCommands();
supplementalDetailsPanel.hidden = true;
detailsPanel.hidden = true;
commandsPanel.hidden = true;
return;
}
// Fill out general info and display it
if (entStates.length == 1)
displaySingle(entStates[0]);
else
displayMultiple(entStates);
// Show basic details.
detailsPanel.hidden = false;
// Fill out commands panel for specific unit selected (or first unit of primary group)
updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel);
// Show health bar for garrisoned units if the garrison panel is visible
if (Engine.GetGUIObjectByName("unitGarrisonPanel") && !Engine.GetGUIObjectByName("unitGarrisonPanel").hidden)
updateGarrisonHealthBar(entStates[0], g_Selection.toList());
}
function getRankIconSprite(entState)
{
if (entState.identity.rank == "Elite")
return "stretched:session/icons/rank3.png";
if (entState.identity.rank == "Advanced")
return "stretched:session/icons/rank2.png";
if (entState.identity.classes.indexOf("CitizenSoldier") != -1)
return "stretched:session/icons/rank1.png";
return "";
}
function tradingGainString(gain, owner)
{
// Translation: Used in the trading gain tooltip
return sprintf(translate("%(gain)s (%(player)s)"), {
"gain": gain,
"player": GetSimState().players[owner].name
});
}
/**
* Returns a message with the details of the trade gain.
*/
function getTradingTooltip(gain)
{
if (!gain)
return "";
let markets = [
{ "gain": gain.market1Gain, "owner": gain.market1Owner },
{ "gain": gain.market2Gain, "owner": gain.market2Owner }
];
let primaryGain = gain.traderGain;
for (let market of markets)
if (market.gain && market.owner == gain.traderOwner)
// Translation: Used in the trading gain tooltip to concatenate profits of different players
primaryGain += translate("+") + market.gain;
let tooltip = tradingGainString(primaryGain, gain.traderOwner);
for (let market of markets)
if (market.gain && market.owner != gain.traderOwner)
tooltip +=
translateWithContext("Separation mark in an enumeration", ", ") +
tradingGainString(market.gain, market.owner);
return tooltip;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 20874)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 20875)
@@ -1,494 +1,494 @@
/**
* @file Contains all helper functions that are needed only for selection_panels.js
* and some that are needed for hotkeys, but not for anything inside input.js.
*/
const UPGRADING_NOT_STARTED = -2;
const UPGRADING_CHOSEN_OTHER = -1;
function canMoveSelectionIntoFormation(formationTemplate)
{
if (!(formationTemplate in g_canMoveIntoFormation))
g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
"formationTemplate": formationTemplate
});
return g_canMoveIntoFormation[formationTemplate];
}
function hasSameRestrictionCategory(templateName1, templateName2)
{
let template1 = GetTemplateData(templateName1);
let template2 = GetTemplateData(templateName2);
if (template1.trainingRestrictions && template2.trainingRestrictions)
return template1.trainingRestrictions.category == template2.trainingRestrictions.category;
if (template1.buildRestrictions && template2.buildRestrictions)
return template1.buildRestrictions.category == template2.buildRestrictions.category;
return false;
}
function getPlayerHighlightColor(player)
{
return "color:" + rgbToGuiColor(g_Players[player].color) + " 160";
}
/**
* Returns a "color:255 0 0 Alpha" string based on how many resources are needed.
*/
function resourcesToAlphaMask(neededResources)
{
let totalCost = 0;
for (let resource in neededResources)
totalCost += +neededResources[resource];
return "color:255 0 0 " + Math.min(125, Math.round(+totalCost / 10) + 50);
}
function getStanceDisplayName(name)
{
switch (name)
{
case "violent":
return translateWithContext("stance", "Violent");
case "aggressive":
return translateWithContext("stance", "Aggressive");
case "defensive":
return translateWithContext("stance", "Defensive");
case "passive":
return translateWithContext("stance", "Passive");
case "standground":
return translateWithContext("stance", "Standground");
default:
warn("Internationalization: Unexpected stance found: " + name);
return name;
}
}
function getStanceTooltip(name)
{
switch (name)
{
case "violent":
return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible");
case "aggressive":
return translateWithContext("stance", "Attack nearby opponents");
case "defensive":
return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location");
case "passive":
return translateWithContext("stance", "Flee if attacked");
case "standground":
return translateWithContext("stance", "Attack opponents in range, but don't move");
default:
return "";
}
}
/**
* Format entity count/limit message for the tooltip
*/
function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
{
if (trainEntLimit == undefined)
return "";
var text = sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), {
"count": trainEntCount,
"limit": trainEntLimit
});
if (trainEntCount >= trainEntLimit)
text = coloredText(text, "red");
for (var c in trainEntLimitChangers)
{
if (!trainEntLimitChangers[c])
continue;
let string = trainEntLimitChangers[c] > 0 ?
translate("%(changer)s enlarges the limit with %(change)s.") :
translate("%(changer)s lessens the limit with %(change)s.");
text += "\n" + sprintf(string, {
"changer": translate(c),
"change": trainEntLimitChangers[c]
});
}
return text;
}
/**
* Format batch training string for the tooltip
* Examples:
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 5"
* buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 10 (2*5)"
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12:
* "Shift-click to train 27 (15 + 12)"
*/
function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
// Don't show the batch training tooltip if either units of this type can't be trained at all
// or only one unit can be trained
if (totalBatchTrainingCount < 2)
return "";
let fullBatchesString = "";
if (buildingsCountToTrainFullBatch > 1)
fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), {
"buildings": buildingsCountToTrainFullBatch,
"batchSize": fullBatchSize
});
else if (buildingsCountToTrainFullBatch == 1)
fullBatchesString = fullBatchSize;
// We need to display the batch details part if there is either more than
// one building with full batch or one building with the full batch and
// another with a partial batch
let batchString;
if (buildingsCountToTrainFullBatch > 1 ||
buildingsCountToTrainFullBatch == 1 && remainderBatch > 0)
if (remainderBatch > 0)
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s).");
else
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s).");
else
batchString = translate("%(action)s to train %(number)s.");
return "[font=\"sans-13\"]" +
coloredText(
sprintf(batchString, {
"action": "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]",
"number": totalBatchTrainingCount,
"fullBatch": fullBatchesString,
"remainderBatch": remainderBatch
}),
g_HotkeyColor) +
"[/font]";
}
/**
* Camera jumping: when the user presses a hotkey the current camera location is marked.
* When pressing another camera jump hotkey the camera jumps back to that position.
* When the camera is already roughly at that location, jump back to where it was previously.
*/
var g_JumpCameraPositions = [];
var g_JumpCameraLast;
function jumpCamera(index)
{
let position = g_JumpCameraPositions[index];
if (!position)
return;
let threshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
if (g_JumpCameraLast &&
Math.abs(Engine.CameraGetX() - position.x) < threshold &&
Math.abs(Engine.CameraGetZ() - position.z) < threshold)
Engine.CameraMoveTo(g_JumpCameraLast.x, g_JumpCameraLast.z);
else
{
g_JumpCameraLast = { "x": Engine.CameraGetX(), "z": Engine.CameraGetZ() };
Engine.CameraMoveTo(position.x, position.z);
}
}
function setJumpCamera(index)
{
g_JumpCameraPositions[index] = { "x": Engine.CameraGetX(), "z": Engine.CameraGetZ() };
}
/**
* Called by GUI when user clicks a research button.
*/
function addResearchToQueue(entity, researchType)
{
Engine.PostNetworkCommand({
"type": "research",
"entity": entity,
"template": researchType
});
}
/**
* Called by GUI when user clicks a production queue item.
*/
function removeFromProductionQueue(entity, id)
{
Engine.PostNetworkCommand({
"type": "stop-production",
"entity": entity,
"id": id
});
}
/**
* Called by unit selection buttons.
*/
function changePrimarySelectionGroup(templateName, deselectGroup)
{
g_Selection.makePrimarySelection(templateName,
Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup);
}
function performCommand(entStates, commandName)
{
if (!entStates.length)
return;
// Don't check all entities, because we assume a player cannot
// select entities from more than one player
if (!controlsPlayer(entStates[0].player) &&
!(g_IsObserver && commandName == "focus-rally"))
return;
if (g_EntityCommands[commandName])
g_EntityCommands[commandName].execute(entStates);
}
function performAllyCommand(entity, commandName)
{
if (!entity)
return;
- let entState = GetExtendedEntityState(entity);
+ let entState = GetEntityState(entity);
let playerState = GetSimState().players[Engine.GetPlayerID()];
if (!playerState.isMutualAlly[entState.player] || g_IsObserver)
return;
if (g_AllyEntityCommands[commandName])
g_AllyEntityCommands[commandName].execute(entState);
}
function performFormation(entities, formationTemplate)
{
if (!entities)
return;
Engine.PostNetworkCommand({
"type": "formation",
"entities": entities,
"name": formationTemplate
});
}
function performStance(entities, stanceName)
{
if (!entities)
return;
Engine.PostNetworkCommand({
"type": "stance",
"entities": entities,
"name": stanceName
});
}
function lockGate(lock)
{
Engine.PostNetworkCommand({
"type": "lock-gate",
"entities": g_Selection.toList(),
"lock": lock
});
}
function packUnit(pack)
{
Engine.PostNetworkCommand({
"type": "pack",
"entities": g_Selection.toList(),
"pack": pack,
"queued": false
});
}
function cancelPackUnit(pack)
{
Engine.PostNetworkCommand({
"type": "cancel-pack",
"entities": g_Selection.toList(),
"pack": pack,
"queued": false
});
}
function upgradeEntity(Template)
{
Engine.PostNetworkCommand({
"type": "upgrade",
"entities": g_Selection.toList(),
"template": Template,
"queued": false
});
}
function cancelUpgradeEntity()
{
Engine.PostNetworkCommand({
"type": "cancel-upgrade",
"entities": g_Selection.toList(),
"queued": false
});
}
/**
* Set the camera to follow the given entity if it's a unit.
* Otherwise stop following.
*/
function setCameraFollow(entity)
{
let entState = entity && GetEntityState(entity);
if (entState && hasClass(entState, "Unit"))
Engine.CameraFollow(entity);
else
Engine.CameraFollow(0);
}
function stopUnits(entities)
{
Engine.PostNetworkCommand({
"type": "stop",
"entities": entities,
"queued": false
});
}
function unloadTemplate(template, owner)
{
Engine.PostNetworkCommand({
"type": "unload-template",
"all": Engine.HotkeyIsPressed("session.unloadtype"),
"template": template,
"owner": owner,
// Filter out all entities that aren't garrisonable.
"garrisonHolders": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && state.garrisonHolder;
})
});
}
function unloadSelection()
{
let parent = 0;
let ents = [];
for (let ent in g_Selection.selected)
{
- let state = GetExtendedEntityState(+ent);
+ let state = GetEntityState(+ent);
if (!state || !state.turretParent)
continue;
if (!parent)
{
parent = state.turretParent;
ents.push(+ent);
}
else if (state.turretParent == parent)
ents.push(+ent);
}
if (parent)
Engine.PostNetworkCommand({
"type": "unload",
"entities": ents,
"garrisonHolder": parent
});
}
function unloadAll()
{
let garrisonHolders = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && state.garrisonHolder;
});
if (!garrisonHolders.length)
return;
let ownEnts = [];
let otherEnts = [];
for (let ent of garrisonHolders)
{
if (controlsPlayer(GetEntityState(ent).player))
ownEnts.push(ent);
else
otherEnts.push(ent);
}
if (ownEnts.length)
Engine.PostNetworkCommand({
"type": "unload-all",
"garrisonHolders": ownEnts
});
if (otherEnts.length)
Engine.PostNetworkCommand({
"type": "unload-all-by-owner",
"garrisonHolders": otherEnts
});
}
function backToWork()
{
Engine.PostNetworkCommand({
"type": "back-to-work",
// Filter out all entities that can't go back to work.
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.hasWorkOrders;
})
});
}
function removeGuard()
{
Engine.PostNetworkCommand({
"type": "remove-guard",
// Filter out all entities that are currently guarding/escorting.
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.isGuarding;
})
});
}
function raiseAlert()
{
Engine.PostNetworkCommand({
"type": "increase-alert-level",
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && state.alertRaiser && !state.alertRaiser.hasRaisedAlert;
})
});
}
function increaseAlertLevel()
{
raiseAlert();
Engine.PostNetworkCommand({
"type": "increase-alert-level",
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && state.alertRaiser && state.alertRaiser.canIncreaseLevel;
})
});
}
function endOfAlert()
{
Engine.PostNetworkCommand({
"type": "alert-end",
"entities": g_Selection.toList().filter(ent => {
let state = GetEntityState(ent);
return state && state.alertRaiser && state.alertRaiser.hasRaisedAlert;
})
});
}
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 20874)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 20875)
@@ -1,1654 +1,1645 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_CivData = loadCivData(false, true);
const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions);
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
var g_GameSpeeds;
/**
* Colors to flash when pop limit reached.
*/
var g_DefaultPopulationColor = "white";
var g_PopulationAlertColor = "orange";
/**
* Seen in the tooltip of the top panel.
*/
var g_ResourceTitleFont = "sans-bold-16";
/**
* A random file will be played. TODO: more variety
*/
var g_Ambient = ["audio/ambient/dayscape/day_temperate_gen_03.ogg"];
/**
* Map, player and match settings set in gamesetup.
*/
const g_GameAttributes = deepfreeze(Engine.GetInitAttributes());
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
var g_IsController;
/**
* True if this is a multiplayer game.
*/
var g_IsNetworked = false;
/**
* Whether we have finished the synchronization and
* can start showing simulation related message boxes.
*/
var g_IsNetworkedActive = false;
/**
* True if the connection to the server has been lost.
*/
var g_Disconnected = false;
/**
* True if the current user has observer capabilities.
*/
var g_IsObserver = false;
/**
* True if the current user has rejoined (or joined the game after it started).
*/
var g_HasRejoined = false;
/**
* Shows a message box asking the user to leave if "won" or "defeated".
*/
var g_ConfirmExit = false;
/**
* True if the current player has paused the game explicitly.
*/
var g_Paused = false;
/**
* The list of GUIDs of players who have currently paused the game, if the game is networked.
*/
var g_PausingClients = [];
/**
* The playerID selected in the change perspective tool.
*/
var g_ViewedPlayer = Engine.GetPlayerID();
/**
* True if the camera should focus on attacks and player commands
* and select the affected units.
*/
var g_FollowPlayer = false;
/**
* Cache the basic player data (name, civ, color).
*/
var g_Players = [];
/**
* Last time when onTick was called().
* Used for animating the main menu.
*/
var g_LastTickTime = Date.now();
/**
* Recalculate which units have their status bars shown with this frequency in milliseconds.
*/
var g_StatusBarUpdate = 200;
/**
* For restoring selection, order and filters when returning to the replay menu
*/
var g_ReplaySelectionData;
var g_PlayerAssignments = {
"local": {
"name": singleplayerName(),
"player": 1
}
};
/**
* Cache dev-mode settings that are frequently or widely used.
*/
var g_DevSettings = {
"changePerspective": false,
"controlAll": false
};
/**
* Whether the entire UI should be hidden (useful for promotional screenshots).
* Can be toggled with a hotkey.
*/
var g_ShowGUI = true;
/**
* Whether status bars should be shown for all of the player's units.
*/
var g_ShowAllStatusBars = false;
/**
* Blink the population counter if the player can't train more units.
*/
var g_IsTrainingBlocked = false;
/**
* Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update).
*/
var g_SimState;
var g_EntityStates = {};
var g_TemplateData = {};
var g_TechnologyData = {};
var g_ResourceData = new Resources();
/**
* Top coordinate of the research list.
* Changes depending on the number of displayed counters.
*/
var g_ResearchListTop = 4;
/**
* List of additional entities to highlight.
*/
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
/**
* Display data of the current players entities shown in the top panel.
*/
var g_PanelEntities = [];
/**
* Order in which the panel entities are shown.
*/
var g_PanelEntityOrder = ["Hero", "Relic"];
/**
* Unit classes to be checked for the idle-worker-hotkey.
*/
var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "CitizenSoldier"];
/**
* Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey.
*/
var g_MilitaryTypes = ["Melee", "Ranged"];
function GetSimState()
{
if (!g_SimState)
g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState"));
return g_SimState;
}
-function GetEntityState(entId)
+function GetMultipleEntityStates(ents)
{
- if (!g_EntityStates[entId])
- {
- g_EntityStates[entId] = Engine.GuiInterfaceCall("GetEntityState", entId);
-
- // Freeze all existing properties, but allow GetExtendedEntityState to extend the object
- if (g_EntityStates[entId])
- for (let name of Object.getOwnPropertyNames(g_EntityStates[entId]))
- if (typeof prop == 'object' && prop !== null)
- deepfreeze(g_EntityStates[entId][name]);
- }
-
- return g_EntityStates[entId];
+ if (!ents.length)
+ return null;
+ let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents);
+ for (let item of entityStates)
+ g_EntityStates[item.entId] = deepfreeze(item.state);
+ return entityStates;
}
-function GetExtendedEntityState(entId)
+function GetEntityState(entId)
{
- let entState = GetEntityState(entId);
- if (entState && !entState.extended)
- {
- let extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId);
- for (let prop in extension)
- entState[prop] = extension[prop];
- entState.extended = true;
- g_EntityStates[entId] = deepfreeze(entState);
- }
+ if (!g_EntityStates[entId])
+ g_EntityStates[entId] = deepfreeze(Engine.GuiInterfaceCall("GetEntityState", entId));
return g_EntityStates[entId];
}
function GetTemplateData(templateName)
{
if (!(templateName in g_TemplateData))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
translateObjectKeys(template, ["specific", "generic", "tooltip"]);
g_TemplateData[templateName] = deepfreeze(template);
}
return g_TemplateData[templateName];
}
function GetTechnologyData(technologyName, civ)
{
if (!g_TechnologyData[civ])
g_TechnologyData[civ] = {};
if (!(technologyName in g_TechnologyData[civ]))
{
let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), civ, g_ResourceData);
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[civ][technologyName] = deepfreeze(template);
}
return g_TechnologyData[civ][technologyName];
}
function init(initData, hotloadData)
{
if (!g_Settings)
{
Engine.EndGame();
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
if (initData)
{
g_IsNetworked = initData.isNetworked;
g_IsController = initData.isController;
g_PlayerAssignments = initData.playerAssignments;
g_ReplaySelectionData = initData.replaySelectionData;
g_HasRejoined = initData.isRejoining;
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
}
else if (g_IsReplay)// Needed for autostart loading option
g_PlayerAssignments.local.player = -1;
LoadModificationTemplates();
updatePlayerData();
Engine.GuiInterfaceCall("UpdateDisplayedPlayerColors");
g_BarterSell = g_ResourceData.GetCodes()[0];
initializeMusic(); // before changing the perspective
initSessionMenuButtons();
for (let slot in Engine.GetGUIObjectByName("panelEntityPanel").children)
initPanelEntities(slot);
updateViewedPlayerDropdown();
// Select "observer" in the view player dropdown when rejoining as a defeated player
let player = g_Players[Engine.GetPlayerID()];
Engine.GetGUIObjectByName("viewPlayer").selected = player && player.state == "defeated" ? 0 : Engine.GetPlayerID() + 1;
// If in Atlas editor, disable the exit button
if (Engine.IsAtlasRunning())
Engine.GetGUIObjectByName("menuExitButton").enabled = false;
if (hotloadData)
g_Selection.selected = hotloadData.selection;
initChatWindow();
sendLobbyPlayerlistUpdate();
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
// Report the performance after 5 seconds (when we're still near
// the initial camera view) and a minute (when the profiler will
// have settled down if framerates as very low), to give some
// extremely rough indications of performance
//
// DISABLED: this information isn't currently useful for anything much,
// and it generates a massive amount of data to transmit and store
// setTimeout(function() { reportPerformance(5); }, 5000);
// setTimeout(function() { reportPerformance(60); }, 60000);
}
function updatePlayerData()
{
let simState = GetSimState();
if (!simState)
return;
let playerData = [];
for (let i = 0; i < simState.players.length; ++i)
{
let playerState = simState.players[i];
playerData.push({
"name": playerState.name,
"civ": playerState.civ,
"color": {
"r": playerState.color.r * 255,
"g": playerState.color.g * 255,
"b": playerState.color.b * 255,
"a": playerState.color.a * 255
},
"team": playerState.team,
"teamsLocked": playerState.teamsLocked,
"cheatsEnabled": playerState.cheatsEnabled,
"state": playerState.state,
"isAlly": playerState.isAlly,
"isMutualAlly": playerState.isMutualAlly,
"isNeutral": playerState.isNeutral,
"isEnemy": playerState.isEnemy,
"guid": undefined, // network guid for players controlled by hosts
"offline": g_Players[i] && !!g_Players[i].offline
});
}
for (let guid in g_PlayerAssignments)
{
let playerID = g_PlayerAssignments[guid].player;
if (!playerData[playerID])
continue;
playerData[playerID].guid = guid;
playerData[playerID].name = g_PlayerAssignments[guid].name;
}
g_Players = playerData;
}
/**
* Depends on the current player (g_IsObserver).
*/
function updateHotkeyTooltips()
{
Engine.GetGUIObjectByName("chatInput").tooltip =
translateWithContext("chat input", "Type the message to send.") + "\n" +
colorizeAutocompleteHotkey() +
colorizeHotkey("\n" + translate("Press %(hotkey)s to open the public chat."), "chat") +
colorizeHotkey(
"\n" + (g_IsObserver ?
translate("Press %(hotkey)s to open the observer chat.") :
translate("Press %(hotkey)s to open the ally chat.")),
"teamchat") +
colorizeHotkey("\n" + translate("Press %(hotkey)s to open the previously selected private chat."), "privatechat");
Engine.GetGUIObjectByName("idleWorkerButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "selection.idleworker") +
translate("Find idle worker");
Engine.GetGUIObjectByName("tradeHelp").tooltip = colorizeHotkey(
translate("Select one type of goods you want to modify by clicking on it, and then use the arrows of the other types to modify their shares. You can also press %(hotkey)s while selecting one type of goods to bring its share to 100%%."),
"session.fulltradeswap");
Engine.GetGUIObjectByName("barterHelp").tooltip = sprintf(
translate("Start by selecting the resource you wish to sell from the upper row. For each time the lower buttons are pressed, %(quantity)s of the upper resource will be sold for the displayed quantity of the lower. Press and hold %(hotkey)s to temporarily multiply the traded amount by %(multiplier)s."), {
"quantity": g_BarterResourceSellQuantity,
"hotkey": colorizeHotkey("%(hotkey)s", "session.massbarter"),
"multiplier": g_BarterMultiplier
});
}
function initPanelEntities(slot)
{
let button = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]");
button.onPress = function() {
let panelEnt = g_PanelEntities.find(ent => ent.slot !== undefined && ent.slot == slot);
if (!panelEnt)
return;
if (!Engine.HotkeyIsPressed("selection.add"))
g_Selection.reset();
g_Selection.addList([panelEnt.ent]);
};
button.onDoublePress = function() {
let panelEnt = g_PanelEntities.find(ent => ent.slot !== undefined && ent.slot == slot);
if (panelEnt)
selectAndMoveTo(getEntityOrHolder(panelEnt.ent));
};
}
/**
* Returns the entity itself except when garrisoned where it returns its garrisonHolder
*/
function getEntityOrHolder(ent)
{
let entState = GetEntityState(ent);
if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length &&
(entState.unitAI.orders[0].type == "Garrison" || entState.unitAI.orders[0].type == "Autogarrison"))
return getEntityOrHolder(entState.unitAI.orders[0].data.target);
return ent;
}
function initializeMusic()
{
initMusic();
if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music)
global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music);
global.music.setState(global.music.states.PEACE);
playAmbient();
}
function updateViewedPlayerDropdown()
{
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
viewPlayer.list_data = [-1].concat(g_Players.map((player, i) => i));
viewPlayer.list = [translate("Observer")].concat(g_Players.map(
(player, i) => colorizePlayernameHelper("■", i) + " " + player.name
));
}
function toggleChangePerspective(enabled)
{
g_DevSettings.changePerspective = enabled;
selectViewPlayer(g_ViewedPlayer);
}
/**
* Change perspective tool.
* Shown to observers or when enabling the developers option.
*/
function selectViewPlayer(playerID)
{
if (playerID < -1 || playerID > g_Players.length - 1)
return;
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay(true);
g_IsObserver = isPlayerObserver(Engine.GetPlayerID());
if (g_IsObserver || g_DevSettings.changePerspective)
{
if (g_ViewedPlayer != playerID)
clearSelection();
g_ViewedPlayer = playerID;
}
if (g_DevSettings.changePerspective)
{
Engine.SetPlayerID(g_ViewedPlayer);
g_IsObserver = isPlayerObserver(g_ViewedPlayer);
}
Engine.SetViewedPlayer(g_ViewedPlayer);
updateTopPanel();
updateChatAddressees();
updateHotkeyTooltips();
updateGameSpeedControl();
// Update GUI and clear player-dependent cache
onSimulationUpdate();
if (g_IsDiplomacyOpen)
openDiplomacy();
if (g_IsTradeOpen)
openTrade();
}
/**
* Returns true if the player with that ID is in observermode.
*/
function isPlayerObserver(playerID)
{
let playerStates = GetSimState().players;
return !playerStates[playerID] || playerStates[playerID].state != "active";
}
/**
* Returns true if the current user can issue commands for that player.
*/
function controlsPlayer(playerID)
{
let playerStates = GetSimState().players;
return playerStates[Engine.GetPlayerID()] &&
playerStates[Engine.GetPlayerID()].controlsAll ||
Engine.GetPlayerID() == playerID &&
playerStates[playerID] &&
playerStates[playerID].state != "defeated";
}
/**
* Called when one or more players have won or were defeated.
*
* @param {array} - IDs of the players who have won or were defeated.
* @param {object} - a plural string stating the victory reason.
* @param {boolean} - whether these players have won or lost.
*/
function playersFinished(players, victoryString, won)
{
addChatMessage({
"type": "defeat-victory",
"message": victoryString,
"players": players
});
if (players.indexOf(Engine.GetPlayerID()) != -1)
reportGame();
sendLobbyPlayerlistUpdate();
updatePlayerData();
updateChatAddressees();
updateGameSpeedControl();
if (players.indexOf(g_ViewedPlayer) == -1)
return;
// Select "observer" item on loss. On win enable observermode without changing perspective
Engine.GetGUIObjectByName("viewPlayer").selected = won ? g_ViewedPlayer + 1 : 0;
if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning())
return;
global.music.setState(
won ?
global.music.states.VICTORY :
global.music.states.DEFEAT
);
g_ConfirmExit = won ? "won" : "defeated";
}
/**
* Sets civ icon for the currently viewed player.
* Hides most gui objects for observers.
*/
function updateTopPanel()
{
let isPlayer = g_ViewedPlayer > 0;
let civIcon = Engine.GetGUIObjectByName("civIcon");
civIcon.hidden = !isPlayer;
if (isPlayer)
{
civIcon.sprite = "stretched:" + g_CivData[g_Players[g_ViewedPlayer].civ].Emblem;
Engine.GetGUIObjectByName("civIconOverlay").tooltip = sprintf(translate("%(civ)s - Structure Tree"), {
"civ": g_CivData[g_Players[g_ViewedPlayer].civ].Name
});
}
Engine.GetGUIObjectByName("optionFollowPlayer").hidden = !g_IsObserver || !isPlayer;
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
viewPlayer.hidden = !g_IsObserver && !g_DevSettings.changePerspective;
let followPlayerLabel = Engine.GetGUIObjectByName("followPlayerLabel");
followPlayerLabel.hidden = Engine.GetTextWidth(followPlayerLabel.font, followPlayerLabel.caption + " ") +
followPlayerLabel.getComputedSize().left > viewPlayer.getComputedSize().left;
let resCodes = g_ResourceData.GetCodes();
let r = 0;
for (let res of resCodes)
{
if (!Engine.GetGUIObjectByName("resource[" + r + "]"))
{
warn("Current GUI limits prevent displaying more than " + r + " resources in the top panel!");
break;
}
Engine.GetGUIObjectByName("resource[" + r + "]_icon").sprite = "stretched:session/icons/resources/" + res + ".png";
Engine.GetGUIObjectByName("resource[" + r + "]").hidden = !isPlayer;
++r;
}
horizontallySpaceObjects("resourceCounts", 5);
hideRemaining("resourceCounts", r);
let resPop = Engine.GetGUIObjectByName("population");
let resPopSize = resPop.size;
resPopSize.left = Engine.GetGUIObjectByName("resource[" + (r - 1) + "]").size.right;
resPop.size = resPopSize;
Engine.GetGUIObjectByName("population").hidden = !isPlayer;
Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("observerText").hidden = isPlayer;
let alphaLabel = Engine.GetGUIObjectByName("alphaLabel");
alphaLabel.hidden = isPlayer && !viewPlayer.hidden;
alphaLabel.size = isPlayer ? "50%+44 0 100%-283 100%" : "155 0 85%-279 100%";
Engine.GetGUIObjectByName("pauseButton").enabled = !g_IsObserver || !g_IsNetworked || g_IsController;
Engine.GetGUIObjectByName("menuResignButton").enabled = !g_IsObserver;
}
function reportPerformance(time)
{
let settings = g_GameAttributes.settings;
Engine.SubmitUserReport("profile", 3, JSON.stringify({
"time": time,
"map": settings.Name,
"seed": settings.Seed, // only defined for random maps
"size": settings.Size, // only defined for random maps
"profiler": Engine.GetProfilerState()
}));
}
/**
* Resign a player.
* @param leaveGameAfterResign If player is quitting after resignation.
*/
function resignGame(leaveGameAfterResign)
{
if (g_IsObserver || g_Disconnected)
return;
Engine.PostNetworkCommand({
"type": "resign"
});
if (!leaveGameAfterResign)
resumeGame(true);
}
/**
* Leave the game
* @param willRejoin If player is going to be rejoining a networked game.
*/
function leaveGame(willRejoin)
{
if (!willRejoin && !g_IsObserver)
resignGame(true);
// Before ending the game
let replayDirectory = Engine.GetCurrentReplayDirectory();
let simData = getReplayMetadata();
let playerID = Engine.GetPlayerID();
Engine.EndGame();
// After the replay file was closed in EndGame
// Done here to keep EndGame small
if (!g_IsReplay)
Engine.AddReplayToCache(replayDirectory);
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_summary.xml", {
"sim": simData,
"gui": {
"dialog": false,
"assignedPlayer": playerID,
"disconnected": g_Disconnected,
"isReplay": g_IsReplay,
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
}
});
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return { "selection": g_Selection.selected };
}
function getSavedGameData()
{
return {
"groups": g_Groups.groups
};
}
function restoreSavedGameData(data)
{
// Restore camera if any
if (data.camera)
Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ,
data.camera.RotX, data.camera.RotY, data.camera.Zoom);
// Clear selection when loading a game
g_Selection.reset();
// Restore control groups
for (let groupNumber in data.groups)
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
}
updateGroups();
}
/**
* Called every frame.
*/
function onTick()
{
if (!g_Settings)
return;
let now = Date.now();
let tickLength = now - g_LastTickTime;
g_LastTickTime = now;
handleNetMessages();
updateCursorAndTooltip();
if (g_Selection.dirty)
{
g_Selection.dirty = false;
+ // When selection changed, get the entityStates of new entities
+ GetMultipleEntityStates(g_Selection.toList().filter(entId => !g_EntityStates[entId]));
updateGUIObjects();
// Display rally points for selected buildings
if (Engine.GetPlayerID() != -1)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength)
recalculateStatusBarDisplay();
updateTimers();
updateMenuPosition(tickLength);
// When training is blocked, flash population (alternates color every 500msec)
Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && now % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor;
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function onWindowResized()
{
// Update followPlayerLabel
updateTopPanel();
resizeChatWindow();
}
function changeGameSpeed(speed)
{
if (!g_IsNetworked)
Engine.SetSimRate(speed);
}
function updateIdleWorkerButton()
{
Engine.GetGUIObjectByName("idleWorkerButton").enabled = Engine.GuiInterfaceCall("HasIdleUnits", {
"viewedPlayer": g_ViewedPlayer,
"idleClasses": g_WorkerTypes,
"excludeUnits": []
});
}
function onSimulationUpdate()
{
// Templates change depending on technologies and auras, so they have to be reloaded every turn.
// g_TechnologyData data never changes, so it shouldn't be deleted.
g_EntityStates = {};
g_TemplateData = {};
g_SimState = undefined;
if (!GetSimState())
return;
+ GetMultipleEntityStates(g_Selection.toList());
+
updateCinemaPath();
handleNotifications();
updateGUIObjects();
for (let type of ["Attack", "Auras", "Heal"])
Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", {
"type": type,
"enabled": Engine.ConfigDB_GetValue("user", "gui.session." + type.toLowerCase() + "range") == "true"
});
if (g_ConfirmExit)
confirmExit();
}
/**
* Don't show the message box before all playerstate changes are processed.
*/
function confirmExit()
{
if (g_IsNetworked && !g_IsNetworkedActive)
return;
closeOpenDialogs();
// Don't ask for exit if other humans are still playing
let isHost = g_IsController && g_IsNetworked;
let askExit = !isHost || isHost && g_Players.every((player, i) =>
i == 0 ||
player.state != "active" ||
g_GameAttributes.settings.PlayerData[i].AI != "");
let subject = g_PlayerStateMessages[g_ConfirmExit];
if (askExit)
subject += "\n" + translate("Do you want to quit?");
messageBox(
400, 200,
subject,
g_ConfirmExit == "won" ?
translate("VICTORIOUS!") :
translate("DEFEATED!"),
askExit ? [translate("No"), translate("Yes")] : [translate("OK")],
askExit ? [resumeGame, leaveGame] : [resumeGame]
);
g_ConfirmExit = false;
}
function updateCinemaPath()
{
let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected;
Engine.GetGUIObjectByName("sn").hidden = !g_ShowGUI || isPlayingCinemaPath;
Engine.Renderer_SetSilhouettesEnabled(!isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true");
}
function updateGUIObjects()
{
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updatePanelEntities();
displayPanelEntities();
updateGroups();
updateDebug();
updatePlayerDisplay();
updateResearchDisplay();
updateSelectionDetails();
updateBuildingPlacementPreview();
updateTimeNotifications();
updateIdleWorkerButton();
if (g_IsTradeOpen)
{
updateTraderTexts();
updateBarterButtons();
}
if (g_ViewedPlayer > 0)
{
let playerState = GetSimState().players[g_ViewedPlayer];
g_DevSettings.controlAll = playerState && playerState.controlsAll;
Engine.GetGUIObjectByName("devControlAll").checked = g_DevSettings.controlAll;
}
if (!g_IsObserver)
{
// Update music state on basis of battle state.
let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer);
if (battleState)
global.music.setState(global.music.states[battleState]);
}
updateDiplomacy();
}
function onReplayFinished()
{
closeOpenDialogs();
pauseGame();
messageBox(400, 200,
translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"),
translateWithContext("replayFinished", "Confirmation"),
[translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")],
[resumeGame, leaveGame]);
}
/**
* updates a status bar on the GUI
* nameOfBar: name of the bar
* points: points to show
* maxPoints: max points
* direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3;
*/
function updateGUIStatusBar(nameOfBar, points, maxPoints, direction)
{
// check, if optional direction parameter is valid.
if (!direction || !(direction >= 0 && direction < 4))
direction = 0;
// get the bar and update it
let statusBar = Engine.GetGUIObjectByName(nameOfBar);
if (!statusBar)
return;
let healthSize = statusBar.size;
let value = 100 * Math.max(0, Math.min(1, points / maxPoints));
// inverse bar
if (direction == 2 || direction == 3)
value = 100 - value;
if (direction == 0)
healthSize.rright = value;
else if (direction == 1)
healthSize.rbottom = value;
else if (direction == 2)
healthSize.rleft = value;
else if (direction == 3)
healthSize.rtop = value;
statusBar.size = healthSize;
}
function updatePanelEntities()
{
let panelEnts =
g_ViewedPlayer == -1 ?
GetSimState().players.reduce((ents, pState) => ents.concat(pState.panelEntities), []) :
GetSimState().players[g_ViewedPlayer].panelEntities;
g_PanelEntities = g_PanelEntities.filter(panelEnt => panelEnts.find(ent => ent == panelEnt.ent));
for (let ent of panelEnts)
{
- let panelEntState = GetExtendedEntityState(ent);
+ let panelEntState = GetEntityState(ent);
let template = GetTemplateData(panelEntState.template);
let panelEnt = g_PanelEntities.find(pEnt => ent == pEnt.ent);
if (!panelEnt)
{
panelEnt = {
"ent": ent,
"tooltip": undefined,
"sprite": "stretched:session/portraits/" + template.icon,
"maxHitpoints": undefined,
"currentHitpoints": panelEntState.hitpoints,
"previousHitpoints": undefined
};
g_PanelEntities.push(panelEnt);
}
panelEnt.tooltip = createPanelEntityTooltip(panelEntState, template);
panelEnt.previousHitpoints = panelEnt.currentHitpoints;
panelEnt.currentHitpoints = panelEntState.hitpoints;
panelEnt.maxHitpoints = panelEntState.maxHitpoints;
}
let panelEntIndex = ent => g_PanelEntityOrder.findIndex(entClass =>
GetEntityState(ent).identity.classes.indexOf(entClass) != -1);
g_PanelEntities = g_PanelEntities.sort((panelEntA, panelEntB) => panelEntIndex(panelEntA.ent) - panelEntIndex(panelEntB.ent));
}
function createPanelEntityTooltip(panelEntState, template)
{
let getPanelEntNameTooltip = panelEntState => "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]";
return [
getPanelEntNameTooltip,
getCurrentHealthTooltip,
getAttackTooltip,
getArmorTooltip,
getEntityTooltip,
getAurasTooltip
].map(tooltip => tooltip(panelEntState)).filter(tip => tip).join("\n");
}
function displayPanelEntities()
{
let buttons = Engine.GetGUIObjectByName("panelEntityPanel").children;
buttons.forEach((button, slot) => {
if (button.hidden || g_PanelEntities.some(ent => ent.slot !== undefined && ent.slot == slot))
return;
button.hidden = true;
stopColorFade("panelEntityHitOverlay[" + slot + "]");
});
// The slot identifies the button, displayIndex determines its position.
for (let displayIndex = 0; displayIndex < Math.min(g_PanelEntities.length, buttons.length); ++displayIndex)
{
let panelEnt = g_PanelEntities[displayIndex];
// Find the first unused slot if new, otherwise reuse previous.
let slot = panelEnt.slot === undefined ?
buttons.findIndex(button => button.hidden) :
panelEnt.slot;
let panelEntButton = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]");
panelEntButton.tooltip = panelEnt.tooltip;
updateGUIStatusBar("panelEntityHealthBar[" + slot + "]", panelEnt.currentHitpoints, panelEnt.maxHitpoints);
if (panelEnt.slot === undefined)
{
let panelEntImage = Engine.GetGUIObjectByName("panelEntityImage[" + slot + "]");
panelEntImage.sprite = panelEnt.sprite;
panelEntButton.hidden = false;
panelEnt.slot = slot;
}
// If the health of the panelEnt changed since the last update, trigger the animation.
if (panelEnt.previousHitpoints > panelEnt.currentHitpoints)
startColorFade("panelEntityHitOverlay[" + slot + "]", 100, 0,
colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);
// TODO: Instead of instant position changes, animate button movement.
setPanelObjectPosition(panelEntButton, displayIndex, buttons.length);
}
}
function updateGroups()
{
g_Groups.update();
// Determine the sum of the costs of a given template
let getCostSum = (ent) => {
let cost = GetTemplateData(GetEntityState(ent).template).cost;
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
};
for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children)
{
Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i;
let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]");
button.hidden = g_Groups.groups[i].getTotalCount() == 0;
button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i);
button.ondoublepress = (function(i) { return function() { performGroup("snap", i); }; })(i);
button.onpressright = (function(i) { return function() { performGroup("breakUp", i); }; })(i);
// Chose icon of the most common template (or the most costly if it's not unique)
if (g_Groups.groups[i].getTotalCount() > 0)
{
let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => {
if (pre.ents.length == cur.ents.length)
return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur;
return pre.ents.length > cur.ents.length ? pre : cur;
}).ents[0]).template).icon;
Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite =
icon ? ("stretched:session/portraits/" + icon) : "groupsIcon";
}
setPanelObjectPosition(button, i, 1);
}
}
function updateDebug()
{
let debug = Engine.GetGUIObjectByName("debugEntityState");
if (!Engine.GetGUIObjectByName("devDisplayState").checked)
{
debug.hidden = true;
return;
}
debug.hidden = false;
let conciseSimState = clone(GetSimState());
conciseSimState.players = "<<>>";
let text = "simulation: " + uneval(conciseSimState);
let selection = g_Selection.toList();
if (selection.length)
{
- let entState = GetExtendedEntityState(selection[0]);
+ let entState = GetEntityState(selection[0]);
if (entState)
{
let template = GetTemplateData(entState.template);
text += "\n\nentity: {\n";
for (let k in entState)
text += " " + k + ":" + uneval(entState[k]) + "\n";
text += "}\n\ntemplate: " + uneval(template);
}
}
debug.caption = text.replace(/\[/g, "\\[");
}
function getAllyStatTooltip(resource)
{
let playersState = GetSimState().players;
let ret = "";
for (let player in playersState)
if (player != 0 &&
player != g_ViewedPlayer &&
g_Players[player].state != "defeated" &&
(g_IsObserver ||
playersState[g_ViewedPlayer].hasSharedLos &&
g_Players[player].isMutualAlly[g_ViewedPlayer]))
ret += "\n" + sprintf(translate("%(playername)s: %(statValue)s"), {
"playername": colorizePlayernameHelper("■", player) + " " + g_Players[player].name,
"statValue": resource == "pop" ?
sprintf(translate("%(popCount)s/%(popLimit)s/%(popMax)s"), playersState[player]) :
Math.round(playersState[player].resourceCounts[resource])
});
return ret;
}
function updatePlayerDisplay()
{
let playerState = GetSimState().players[g_ViewedPlayer];
if (!playerState)
return;
let resCodes = g_ResourceData.GetCodes();
for (let r = 0; r < resCodes.length; ++r)
{
let resourceObj = Engine.GetGUIObjectByName("resource[" + r + "]");
if (!resourceObj)
break;
let res = resCodes[r];
let tooltip = '[font="' + g_ResourceTitleFont + '"]' +
resourceNameFirstWord(res) + '[/font]';
let descr = g_ResourceData.GetResource(res).description;
if (descr)
tooltip += "\n" + translate(descr);
tooltip += getAllyStatTooltip(res);
resourceObj.tooltip = tooltip;
Engine.GetGUIObjectByName("resource[" + r + "]_count").caption = Math.floor(playerState.resourceCounts[res]);
}
Engine.GetGUIObjectByName("resourcePop").caption = sprintf(translate("%(popCount)s/%(popLimit)s"), playerState);
Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" +
sprintf(translate("Maximum population: %(popCap)s"), { "popCap": playerState.popMax }) +
getAllyStatTooltip("pop");
g_IsTrainingBlocked = playerState.trainingBlocked;
}
function selectAndMoveTo(ent)
{
let entState = GetEntityState(ent);
if (!entState || !entState.position)
return;
g_Selection.reset();
g_Selection.addList([ent]);
let position = entState.position;
Engine.CameraMoveTo(position.x, position.z);
}
function updateResearchDisplay()
{
let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer);
// Set up initial positioning.
let buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right;
for (let i = 0; i < 10; ++i)
{
let button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]");
let size = button.size;
size.top = g_ResearchListTop + (4 + buttonSideLength) * i;
size.bottom = size.top + buttonSideLength;
button.size = size;
}
let numButtons = 0;
for (let tech in researchStarted)
{
// Show at most 10 in-progress techs.
if (numButtons >= 10)
break;
let template = GetTechnologyData(tech, g_Players[g_ViewedPlayer].civ);
let button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]");
button.hidden = false;
button.tooltip = getEntityNames(template);
button.onpress = (function(e) { return function() { selectAndMoveTo(e); }; })(researchStarted[tech].researcher);
let icon = "stretched:session/portraits/" + template.icon;
Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon;
// Scale the progress indicator.
let size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left));
Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size;
++numButtons;
}
// Hide unused buttons.
for (let i = numButtons; i < 10; ++i)
Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true;
}
/**
* Toggles the display of status bars for all of the player's entities.
*
* @param {Boolean} remove - Whether to hide all previously shown status bars.
*/
function recalculateStatusBarDisplay(remove = false)
{
let entities;
if (g_ShowAllStatusBars && !remove)
entities = g_ViewedPlayer == -1 ?
Engine.PickNonGaiaEntitiesOnScreen() :
Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer);
else
{
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
// Remove selected entities from the 'all entities' array,
// to avoid disabling their status bars.
entities = Engine.GuiInterfaceCall(
g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", {
"viewedPlayer": g_ViewedPlayer
}).filter(idx => selected.indexOf(idx) == -1);
}
Engine.GuiInterfaceCall("SetStatusBars", {
"entities": entities,
"enabled": g_ShowAllStatusBars && !remove
});
}
/**
* Inverts the given configuration boolean and returns the current state.
* For example "silhouettes".
*/
function toggleConfigBool(configName)
{
let enabled = Engine.ConfigDB_GetValue("user", configName) != "true";
Engine.ConfigDB_CreateValue("user", configName, String(enabled));
Engine.ConfigDB_WriteValueToFile("user", configName, String(enabled), "config/user.cfg");
return enabled;
}
/**
* Toggles the display of range overlays of selected entities for the given range type.
* @param {string} type - for example "Auras"
*/
function toggleRangeOverlay(type)
{
let enabled = toggleConfigBool("gui.session." + type.toLowerCase() + "range");
Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", {
"type": type,
"enabled": enabled
});
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
Engine.GuiInterfaceCall("SetRangeOverlays", {
"entities": selected,
"enabled": enabled
});
}
// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
let entsAdd = []; // list of entities units to be highlighted
let entsRemove = [];
let highlighted = g_Selection.toList();
for (let ent in g_Selection.highlighted)
highlighted.push(g_Selection.highlighted[ent]);
if (g_ShowGuarding)
// flag the guarding entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.guard || !state.guard.entities.length)
continue;
for (let ent of state.guard.entities)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
if (g_ShowGuarded)
// flag the guarded entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.unitAI || !state.unitAI.isGuarding)
continue;
let ent = state.unitAI.isGuarding;
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
// flag the entities to remove (from the previously added) from this additional highlight
for (let ent of g_AdditionalHighlight)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
entsRemove.push(ent);
_setHighlight(entsAdd, g_HighlightedAlpha, true);
_setHighlight(entsRemove, 0, false);
g_AdditionalHighlight = entsAdd;
}
function playAmbient()
{
Engine.PlayAmbientSound(pickRandom(g_Ambient), true);
}
function getBuildString()
{
return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), {
"buildDate": Engine.GetBuildTimestamp(0),
"revision": Engine.GetBuildTimestamp(2)
});
}
function showTimeWarpMessageBox()
{
messageBox(
500, 250,
translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."),
translate("Time warp mode")
);
}
/**
* Adds the ingame time and ceasefire counter to the global FPS and
* realtime counters shown in the top right corner.
*/
function appendSessionCounters(counters)
{
let simState = GetSimState();
if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true")
{
let currentSpeed = Engine.GetSimRate();
if (currentSpeed != 1.0)
// Translation: The "x" means "times", with the mathematical meaning of multiplication.
counters.push(sprintf(translate("%(time)s (%(speed)sx)"), {
"time": timeToString(simState.timeElapsed),
"speed": Engine.FormatDecimalNumberIntoString(currentSpeed)
}));
else
counters.push(timeToString(simState.timeElapsed));
}
if (simState.ceasefireActive && Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true")
counters.push(timeToString(simState.ceasefireTimeRemaining));
g_ResearchListTop = 4 + 14 * counters.length;
}
/**
* Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function sendLobbyPlayerlistUpdate()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
// Extract the relevant player data and minimize packet load
let minPlayerData = [];
for (let playerID in g_GameAttributes.settings.PlayerData)
{
if (+playerID == 0)
continue;
let pData = g_GameAttributes.settings.PlayerData[playerID];
let minPData = { "Name": pData.Name };
if (g_GameAttributes.settings.LockTeams)
minPData.Team = pData.Team;
if (pData.AI)
{
minPData.AI = pData.AI;
minPData.AIDiff = pData.AIDiff;
}
if (g_Players[playerID].offline)
minPData.Offline = true;
// Whether the player has won or was defeated
let state = g_Players[playerID].state;
if (state != "active")
minPData.State = state;
minPlayerData.push(minPData);
}
// Add observers
let connectedPlayers = 0;
for (let guid in g_PlayerAssignments)
{
let pData = g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player];
if (pData)
++connectedPlayers;
else
minPlayerData.push({
"Name": g_PlayerAssignments[guid].name,
"Team": "observer"
});
}
Engine.SendChangeStateGame(connectedPlayers, playerDataToStringifiedTeamList(minPlayerData));
}
/**
* Send a report on the gamestatus to the lobby.
*/
function reportGame()
{
// Only 1v1 games are rated (and Gaia is part of g_Players)
if (!Engine.HasXmppClient() || !Engine.IsRankedGame() ||
g_Players.length != 3 || Engine.GetPlayerID() == -1)
return;
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
let unitsClasses = [
"total",
"Infantry",
"Worker",
"FemaleCitizen",
"Cavalry",
"Champion",
"Hero",
"Siege",
"Ship",
"Trader"
];
let unitsCountersTypes = [
"unitsTrained",
"unitsLost",
"enemyUnitsKilled"
];
let buildingsClasses = [
"total",
"CivCentre",
"House",
"Economic",
"Outpost",
"Military",
"Fortress",
"Wonder"
];
let buildingsCountersTypes = [
"buildingsConstructed",
"buildingsLost",
"enemyBuildingsDestroyed"
];
let resourcesTypes = [
"wood",
"food",
"stone",
"metal"
];
let resourcesCounterTypes = [
"resourcesGathered",
"resourcesUsed",
"resourcesSold",
"resourcesBought"
];
let misc = [
"tradeIncome",
"tributesSent",
"tributesReceived",
"treasuresCollected",
"lootCollected",
"percentMapExplored"
];
let playerStatistics = {};
// Unit Stats
for (let unitCounterType of unitsCountersTypes)
{
if (!playerStatistics[unitCounterType])
playerStatistics[unitCounterType] = { };
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] = "";
}
playerStatistics.unitsLostValue = "";
playerStatistics.unitsKilledValue = "";
// Building stats
for (let buildingCounterType of buildingsCountersTypes)
{
if (!playerStatistics[buildingCounterType])
playerStatistics[buildingCounterType] = { };
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] = "";
}
playerStatistics.buildingsLostValue = "";
playerStatistics.enemyBuildingsDestroyedValue = "";
// Resources
for (let resourcesCounterType of resourcesCounterTypes)
{
if (!playerStatistics[resourcesCounterType])
playerStatistics[resourcesCounterType] = { };
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] = "";
}
playerStatistics.resourcesGathered.vegetarianFood = "";
for (let type of misc)
playerStatistics[type] = "";
// Total
playerStatistics.economyScore = "";
playerStatistics.militaryScore = "";
playerStatistics.totalScore = "";
let mapName = g_GameAttributes.settings.Name;
let playerStates = "";
let playerCivs = "";
let teams = "";
let teamsLocked = true;
// Serialize the statistics for each player into a comma-separated list.
// Ignore gaia
for (let i = 1; i < extendedSimState.players.length; ++i)
{
let player = extendedSimState.players[i];
let maxIndex = player.sequences.time.length - 1;
playerStates += player.state + ",";
playerCivs += player.civ + ",";
teams += player.team + ",";
teamsLocked = teamsLocked && player.teamsLocked;
for (let resourcesCounterType of resourcesCounterTypes)
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] += player.sequences[resourcesCounterType][resourcesType][maxIndex] + ",";
playerStatistics.resourcesGathered.vegetarianFood += player.sequences.resourcesGathered.vegetarianFood[maxIndex] + ",";
for (let unitCounterType of unitsCountersTypes)
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] += player.sequences[unitCounterType][unitsClass][maxIndex] + ",";
for (let buildingCounterType of buildingsCountersTypes)
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] += player.sequences[buildingCounterType][buildingsClass][maxIndex] + ",";
let total = 0;
for (let type in player.sequences.resourcesGathered)
total += player.sequences.resourcesGathered[type][maxIndex];
playerStatistics.economyScore += total + ",";
playerStatistics.militaryScore += Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] +
player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10) + ",";
playerStatistics.totalScore += (total + Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] +
player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10)) + ",";
for (let type of misc)
playerStatistics[type] += player.sequences[type][maxIndex] + ",";
}
// Send the report with serialized data
let reportObject = {};
reportObject.timeElapsed = extendedSimState.timeElapsed;
reportObject.playerStates = playerStates;
reportObject.playerID = Engine.GetPlayerID();
reportObject.matchID = g_GameAttributes.matchID;
reportObject.civs = playerCivs;
reportObject.teams = teams;
reportObject.teamsLocked = String(teamsLocked);
reportObject.ceasefireActive = String(extendedSimState.ceasefireActive);
reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining);
reportObject.mapName = mapName;
reportObject.economyScore = playerStatistics.economyScore;
reportObject.militaryScore = playerStatistics.militaryScore;
reportObject.totalScore = playerStatistics.totalScore;
for (let rct of resourcesCounterTypes)
for (let rt of resourcesTypes)
reportObject[rt + rct.substr(9)] = playerStatistics[rct][rt];
// eg. rt = food rct.substr = Gathered rct = resourcesGathered
reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood;
for (let type of unitsClasses)
{
// eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "UnitsTrained"] = playerStatistics.unitsTrained[type];
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "UnitsLost"] = playerStatistics.unitsLost[type];
reportObject["enemy" + type + "UnitsKilled"] = playerStatistics.enemyUnitsKilled[type];
}
for (let type of buildingsClasses)
{
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "BuildingsConstructed"] = playerStatistics.buildingsConstructed[type];
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "BuildingsLost"] = playerStatistics.buildingsLost[type];
reportObject["enemy" + type + "BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type];
}
for (let type of misc)
reportObject[type] = playerStatistics[type];
Engine.SendGameReport(reportObject);
}
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 20874)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 20875)
@@ -1,1540 +1,1540 @@
/**
* Specifies which template should indicate the target location of a player command,
* given a command type.
*/
var g_TargetMarker = {
"move": "special/target_marker"
};
/**
* Which enemy entity types will be attacked on sight when patroling.
*/
var g_PatrolTargets = ["Unit"];
/**
* List of different actions units can execute,
* this is mostly used to determine which actions can be executed
*
* "execute" is meant to send the command to the engine
*
* The next functions will always return false
* in case you have to continue to seek
* (i.e. look at the next entity for getActionInfo, the next
* possible action for the actionCheck ...)
* They will return an object when the searching is finished
*
* "getActionInfo" is used to determine if the action is possible,
* and also give visual feedback to the user (tooltips, cursors, ...)
*
* "preSelectedActionCheck" is used to select actions when the gui buttons
* were used to set them, but still require a target (like the guard button)
*
* "hotkeyActionCheck" is used to check the possibility of actions when
* a hotkey is pressed
*
* "actionCheck" is used to check the possibilty of actions without specific
* command. For that, the specificness variable is used
*
* "specificness" is used to determine how specific an action is,
* The lower the number, the more specific an action is, and the bigger
* the chance of selecting that action when multiple actions are possible
*/
var g_UnitActions =
{
"move":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "walk",
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
if (!someUnitAI(selection) ||
!Engine.HotkeyIsPressed("session.move") ||
!getActionInfo("move", target, selection).possible)
return false;
return { "type": "move" };
},
"actionCheck": function(target, selection)
{
if (!someUnitAI(selection) || !getActionInfo("move", target, selection).possible)
return false;
return { "type": "move" };
},
"specificness": 12,
},
"attack-move":
{
"execute": function(target, action, selection, queued)
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
Engine.PostNetworkCommand({
"type": "attack-walk",
"entities": selection,
"x": target.x,
"z": target.z,
"targetClasses": targetClasses,
"queued": queued
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
if (!someUnitAI(selection) ||
!Engine.HotkeyIsPressed("session.attackmove") ||
!getActionInfo("attack-move", target, selection).possible)
return false;
return {
"type": "attack-move",
"cursor": "action-attack-move"
};
},
"specificness": 30,
},
"capture":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"allowCapture": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState.hitpoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["Capture"]
})
};
},
"actionCheck": function(target, selection)
{
if (!getActionInfo("capture", target, selection).possible)
return false;
return {
"type": "capture",
"cursor": "action-capture",
"target": target
};
},
"specificness": 9,
},
"attack":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"queued": queued,
"allowCapture": false
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState.hitpoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["!Capture"]
})
};
},
"hotkeyActionCheck": function(target, selection)
{
if (!Engine.HotkeyIsPressed("session.attack") ||
!getActionInfo("attack", target, selection).possible)
return false;
return {
"type": "attack",
"cursor": "action-attack",
"target": target
};
},
"actionCheck": function(target, selection)
{
if (!getActionInfo("attack", target, selection).possible)
return false;
return {
"type": "attack",
"cursor": "action-attack",
"target": target
};
},
"specificness": 10,
},
"patrol":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "patrol",
"entities": selection,
"x": target.x,
"z": target.z,
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
"queued": queued,
"allowCapture": false
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", { "name": "order_patrol", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.canPatrol)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
if (!someCanPatrol(selection) ||
!Engine.HotkeyIsPressed("session.patrol") ||
!getActionInfo("patrol", target, selection).possible)
return false;
return {
"type": "patrol",
"cursor": "action-patrol",
"target": target
};
},
"preSelectedActionCheck": function(target, selection)
{
if (preSelectedAction != ACTION_PATROL || !getActionInfo("patrol", target, selection).possible)
return false;
return {
"type": "patrol",
"cursor": "action-patrol",
"target": target
};
},
"specificness": 37,
},
"heal":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "heal",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_heal",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.heal ||
!hasClass(targetState, "Unit") || !targetState.needsHeal ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
entState.id == targetState.id) // Healers can't heal themselves.
return false;
let unhealableClasses = entState.heal.unhealableClasses;
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
return false;
let healableClasses = entState.heal.healableClasses;
if (!MatchesClassList(targetState.identity.classes, healableClasses))
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
if (!getActionInfo("heal", target, selection).possible)
return false;
return {
"type": "heal",
"cursor": "action-heal",
"target": target
};
},
"specificness": 7,
},
"repair":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "repair",
"entities": selection,
"target": action.target,
"autocontinue": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_repair",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.builder ||
!targetState.needsRepair && !targetState.foundation ||
!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
return { "possible": true };
},
"preSelectedActionCheck": function(target, selection)
{
if (preSelectedAction != ACTION_REPAIR)
return false;
if (getActionInfo("repair", target, selection).possible)
return {
"type": "repair",
"cursor": "action-repair",
"target": target
};
return {
"type": "none",
"cursor": "action-repair-disabled",
"target": null
};
},
"hotkeyActionCheck": function(target, selection)
{
if (!Engine.HotkeyIsPressed("session.repair") ||
!getActionInfo("repair", target, selection).possible)
return false;
return {
"type": "repair",
"cursor": "action-repair",
"target": target
};
},
"actionCheck": function(target, selection)
{
if (!getActionInfo("repair", target, selection).possible)
return false;
return {
"type": "repair",
"cursor": "action-repair",
"target": target
};
},
"specificness": 11,
},
"gather":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "gather",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceSupply)
return false;
let resource = findGatherType(entState, targetState.resourceSupply);
if (!resource)
return false;
return {
"possible": true,
"cursor": "action-gather-" + resource
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("gather", target, selection);
if (!actionInfo.possible)
return false;
return {
"type": "gather",
"cursor": actionInfo.cursor,
"target": target
};
},
"specificness": 1,
},
"returnresource":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "returnresource",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceDropsite)
return false;
let playerState = GetSimState().players[entState.player];
if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
{
if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
}
else if (!playerCheck(entState, targetState, ["Player"]))
return false;
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
return false;
let carriedType = entState.resourceCarrying[0].type;
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
return false;
return {
"possible": true,
"cursor": "action-return-" + carriedType
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("returnresource", target, selection);
if (!actionInfo.possible)
return false;
return {
"type": "returnresource",
"cursor": actionInfo.cursor,
"target": target
};
},
"specificness": 2,
},
"setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "setup-trade-route",
"entities": selection,
"target": action.target,
"source": null,
"route": null,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_trade",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails)
return false;
let tooltip;
switch (tradingDetails.type)
{
case "is first":
tooltip = translate("Origin trade market.") + "\n";
if (tradingDetails.hasBothMarkets)
tooltip += sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
else
tooltip += translate("Right-click on another market to set it as a destination trade market.");
break;
case "is second":
tooltip = translate("Destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
case "set first":
tooltip = translate("Right-click to set as origin trade market");
break;
case "set second":
if (tradingDetails.gain.traderGain == 0) // markets too close
return false;
tooltip = translate("Right-click to set as destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
}
return {
"possible": true,
"tooltip": tooltip
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("setup-trade-route", target, selection);
if (!actionInfo.possible)
return false;
return {
"type": "setup-trade-route",
"cursor": "action-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target
};
},
"specificness": 0,
},
"garrison":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "garrison",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.canGarrison || !targetState.garrisonHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.garrisonedEntitiesCount,
"capacity": targetState.garrisonHolder.capacity
});
let extraCount = 0;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.garrisonedEntitiesCount;
if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
return false;
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
if (preSelectedAction != ACTION_GARRISON)
return false;
let actionInfo = getActionInfo("garrison", target, selection);
if (!actionInfo.possible)
return {
"type": "none",
"cursor": "action-garrison-disabled",
"target": null
};
return {
"type": "garrison",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target
};
},
"hotkeyActionCheck": function(target, selection)
{
let actionInfo = getActionInfo("garrison", target, selection);
if (!Engine.HotkeyIsPressed("session.garrison") || !actionInfo.possible)
return false;
return {
"type": "garrison",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target
};
},
"specificness": 20,
},
"guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "guard",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.guard ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
!entState.unitAI || !entState.unitAI.canGuard ||
targetState.unitAI && targetState.unitAI.isGuarding)
return false;
return { "possible": true };
},
"preSelectedActionCheck": function(target, selection)
{
if (preSelectedAction != ACTION_GUARD)
return false;
if (getActionInfo("guard", target, selection).possible)
return {
"type": "guard",
"cursor": "action-guard",
"target": target
};
return {
"type": "none",
"cursor": "action-guard-disabled",
"target": null
};
},
"hotkeyActionCheck": function(target, selection)
{
if (!Engine.HotkeyIsPressed("session.guard") ||
!getActionInfo("guard", target, selection).possible)
return false;
return {
"type": "guard",
"cursor": "action-guard",
"target": target
};
},
"specificness": 40,
},
"remove-guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "remove-guard",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": selection[0]
});
return true;
},
"hotkeyActionCheck": function(target, selection)
{
if (!Engine.HotkeyIsPressed("session.guard") ||
!getActionInfo("remove-guard", target, selection).possible ||
!someGuarding(selection))
return false;
return {
"type": "remove-guard",
"cursor": "action-remove-guard"
};
},
"specificness": 41,
},
"set-rallypoint":
{
"execute": function(target, action, selection, queued)
{
// if there is a position set in the action then use this so that when setting a
// rally point on an entity it is centered on that entity
if (action.position)
target = action.position;
Engine.PostNetworkCommand({
"type": "set-rallypoint",
"entities": selection,
"x": target.x,
"z": target.z,
"data": action.data,
"queued": queued
});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
let tooltip;
// default to walking there (or attack-walking if hotkey pressed)
let data = { "command": "walk" };
let cursor = "";
if (Engine.HotkeyIsPressed("session.attackmove"))
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
data.command = "attack-walk";
data.targetClasses = targetClasses;
cursor = "action-attack-move";
}
if (Engine.HotkeyIsPressed("session.repair") &&
(targetState.needsRepair || targetState.foundation) &&
playerCheck(entState, targetState, ["Player", "Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState.garrisonHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "garrison";
data.target = targetState.id;
cursor = "action-garrison";
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.garrisonedEntitiesCount,
"capacity": targetState.garrisonHolder.capacity
});
if (targetState.garrisonHolder.garrisonedEntitiesCount >=
targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState.resourceSupply)
{
let resourceType = targetState.resourceSupply.type;
if (resourceType.generic == "treasure")
cursor = "action-gather-" + resourceType.generic;
else
cursor = "action-gather-" + resourceType.specific;
data.command = "gather-near-position";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
if (!targetState.speed)
{
data.command = "gather";
data.target = targetState.id;
}
}
else if (entState.market && targetState.market &&
entState.id != targetState.id &&
(!entState.market.naval || targetState.market.naval) &&
!playerCheck(entState, targetState, ["Enemy"]))
{
// Find a trader (if any) that this building can produce.
let trader;
if (entState.production && entState.production.entities.length)
for (let i = 0; i < entState.production.entities.length; ++i)
if ((trader = GetTemplateData(entState.production.entities[i]).trader))
break;
let traderData = {
"firstMarket": entState.id,
"secondMarket": targetState.id,
"template": trader
};
let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
if (gain && gain.traderGain)
{
data.command = "trade";
data.target = traderData.secondMarket;
data.source = traderData.firstMarket;
cursor = "action-setup-trade-route";
tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
sprintf(
trader ?
translate("Gain: %(gain)s") :
translate("Expected gain: %(gain)s"),
{ "gain": getTradingTooltip(gain) });
}
}
else if ((targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (playerCheck(entState, targetState, ["Enemy"]))
{
data.target = targetState.id;
data.command = "attack";
cursor = "action-attack";
}
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities
if (!Engine.HotkeyIsPressed("session.autorallypoint") ||
!targetState.production ||
!targetState.production.entities.length)
for (let ent in g_Selection.selected)
if (targetState.id == +ent)
return false;
return {
"possible": true,
"data": data,
"position": targetState.position,
"cursor": cursor,
"tooltip": tooltip
};
},
"actionCheck": function(target, selection)
{
if (someUnitAI(selection) || !someRallyPoints(selection))
return false;
let actionInfo = getActionInfo("set-rallypoint", target, selection);
if (!actionInfo.possible)
return false;
return {
"type": "set-rallypoint",
"cursor": actionInfo.cursor,
"data": actionInfo.data,
"tooltip": actionInfo.tooltip,
"position": actionInfo.position
};
},
"specificness": 6,
},
"unset-rallypoint":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "unset-rallypoint",
"entities": selection
});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (entState.id != targetState.id ||
!entState.rallyPoint || !entState.rallyPoint.position)
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
if (someUnitAI(selection) || !someRallyPoints(selection) ||
!getActionInfo("unset-rallypoint", target, selection).possible)
return false;
return {
"type": "unset-rallypoint",
"cursor": "action-unset-rally"
};
},
"specificness": 11,
},
"none":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"specificness": 100,
},
};
/**
* Info and actions for the entity commands
* Currently displayed in the bottom of the central panel
*/
var g_EntityCommands =
{
"unload-all": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
if (entState.garrisonHolder)
count += entState.garrisonHolder.entities.length;
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
translate("Unload All."),
"icon": "garrison-out.png",
"count": count,
};
},
"execute": function()
{
unloadAll();
},
},
"delete": {
"getInfo": function(entStates)
{
return entStates.some(entState => !isUndeletable(entState)) ?
{
"tooltip":
colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
translate("Destroy the selected units or buildings.") + "\n" +
colorizeHotkey(
translate("Use %(hotkey)s to avoid the confirmation dialog."),
"session.noconfirmation"
),
"icon": "kill_small.png"
} :
{
// Get all delete reasons and remove duplications
"tooltip": entStates.map(entState => isUndeletable(entState))
.filter((reason, pos, self) =>
self.indexOf(reason) == pos && reason
).join("\n"),
"icon": "kill_small_disabled.png"
};
},
"execute": function(entStates)
{
if (!entStates.length || entStates.every(entState => isUndeletable(entState)))
return;
if (Engine.HotkeyIsPressed("session.noconfirmation"))
Engine.PostNetworkCommand({
"type": "delete-entities",
"entities": entStates.map(entState => entState.id)
});
else
openDeleteDialog(entStates.map(entState => entState.id));
},
},
"stop": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
translate("Abort the current order."),
"icon": "stop.png"
};
},
"execute": function(entStates)
{
if (entStates.length)
stopUnits(entStates.map(entState => entState.id));
},
},
"garrison": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || entState.turretParent))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
translate("Order the selected units to garrison in a building or unit."),
"icon": "garrison.png"
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
},
},
"unload": {
"getInfo": function(entStates)
{
if (entStates.every(entState => {
if (!entState.unitAI || !entState.turretParent)
return true;
let parent = GetEntityState(entState.turretParent);
return !parent || !parent.garrisonHolder || parent.garrisonHolder.entities.indexOf(entState.id) == -1;
}))
return false;
return {
"tooltip": translate("Unload"),
"icon": "garrison-out.png"
};
},
"execute": function()
{
unloadSelection();
},
},
"repair": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.builder))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
translate("Order the selected units to repair a building or mechanical unit."),
"icon": "repair.png"
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
},
},
"focus-rally": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.rallyPoint))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
translate("Focus on Rally Point."),
"icon": "focus-rally.png"
};
},
"execute": function(entStates)
{
// TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
let focusTarget;
for (let entState of entStates)
if (entState.rallyPoint && entState.rallyPoint.position)
{
focusTarget = entState.rallyPoint.position;
break;
}
if (!focusTarget)
for (let entState of entStates)
if (entState.position)
{
focusTarget = entState.position;
break;
}
if (focusTarget)
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
},
},
"back-to-work": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
translate("Back to Work"),
"icon": "production.png"
};
},
"execute": function()
{
backToWork();
},
},
"add-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState =>
!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
translate("Order the selected units to guard a building or unit."),
"icon": "add-guard.png"
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
},
},
"remove-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
return false;
return {
"tooltip": translate("Remove guard"),
"icon": "remove-guard.png"
};
},
"execute": function()
{
removeGuard();
},
},
"select-trading-goods": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.market))
return false;
return {
"tooltip": translate("Barter & Trade"),
"icon": "economics.png"
};
},
"execute": function()
{
toggleTrade();
},
},
"patrol": {
"getInfo": function(entStates)
{
if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
translate("Patrol") + "\n" +
translate("Attack all encountered enemy units while avoiding buildings."),
"icon": "patrol.png"
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_PATROL;
},
},
"share-dropsite": {
"getInfo": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!sharableEntities.length)
return false;
// Returns if none of the entities belong to a player with a mutual ally
if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
(isAlly, playerId) => isAlly && playerId != entState.player)))
return false;
return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
{
"tooltip": translate("Press to allow allies to use this dropsite"),
"icon": "locked_small.png"
} :
{
"tooltip": translate("Press to prevent allies from using this dropsite"),
"icon": "unlocked_small.png"
};
},
"execute": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
Engine.PostNetworkCommand({
"type": "set-dropsite-sharing",
"entities": sharableEntities.map(entState => entState.id),
"shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
});
},
}
};
var g_AllyEntityCommands =
{
"unload-all": {
"getInfo": function(entState)
{
if (!entState.garrisonHolder)
return false;
let player = Engine.GetPlayerID();
let count = 0;
for (let ent in g_Selection.selected)
{
let selectedEntState = GetEntityState(+ent);
if (!selectedEntState.garrisonHolder)
continue;
for (let entity of selectedEntState.garrisonHolder.entities)
{
let state = GetEntityState(entity);
if (state.player == player)
++count;
}
}
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
translate("Unload All."),
"icon": "garrison-out.png",
"count": count,
};
},
"execute": function(entState)
{
unloadAll();
},
},
"share-dropsite": {
"getInfo": function(entState)
{
if (Engine.GetPlayerID() == -1 ||
!GetSimState().players[Engine.GetPlayerID()].hasSharedDropsites ||
!entState.resourceDropsite || !entState.resourceDropsite.sharable)
return false;
if (entState.resourceDropsite.shared)
return {
"tooltip": translate("You are allowed to use this dropsite"),
"icon": "unlocked_small.png"
};
return {
"tooltip": translate("The use of this dropsite is prohibited"),
"icon": "locked_small.png"
};
},
"execute": function(entState)
{
// This command button is always disabled
},
}
};
function playerCheck(entState, targetState, validPlayers)
{
let playerState = GetSimState().players[entState.player];
for (let player of validPlayers)
if (player == "Gaia" && targetState.player == 0 ||
player == "Player" && targetState.player == entState.player ||
playerState["is" + player] && playerState["is" + player][targetState.player])
return true;
return false;
}
function hasClass(entState, className)
{
// note: use the functions in globalscripts/Templates.js for more versatile matching
return entState.identity && entState.identity.classes.indexOf(className) != -1;
}
/**
* Work out whether at least part of the selected entities have UnitAI.
*/
function someUnitAI(entities)
{
return entities.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.unitAI;
});
}
function someRallyPoints(entities)
{
return entities.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.rallyPoint;
});
}
function someGuarding(entities)
{
return entities.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.unitAI && entState.unitAI.isGuarding;
});
}
function someCanPatrol(entities)
{
return entities.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.unitAI && entState.unitAI.canPatrol;
});
}
/**
* Keep in sync with Commands.js.
*/
function isUndeletable(entState)
{
if (g_DevSettings.controlAll)
return false;
if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
return translate("The entity has to be killed before it can be gathered from");
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
return translate("You cannot destroy this entity as you own less than half the capture points");
if (!entState.identity.canDelete)
return translate("This entity is undeletable");
return false;
}
function DrawTargetMarker(target)
{
Engine.GuiInterfaceCall("AddTargetMarker", {
"template": g_TargetMarker.move,
"x": target.x,
"z": target.z
});
}
function findGatherType(gatherer, supply)
{
if (!gatherer.resourceGatherRates || !supply)
return undefined;
if (gatherer.resourceGatherRates[supply.type.generic + "." + supply.type.specific])
return supply.type.specific;
if (gatherer.resourceGatherRates[supply.type.generic])
return supply.type.generic;
return undefined;
}
function getActionInfo(action, target, selection)
{
let simState = GetSimState();
// If the selection doesn't exist, no action
if (!GetEntityState(selection[0]))
return { "possible": false };
if (!target) // TODO move these non-target actions to an object like unit_actions.js
{
if (action == "set-rallypoint")
{
let cursor = "";
let data = { "command": "walk" };
if (Engine.HotkeyIsPressed("session.attackmove"))
{
data.command = "attack-walk";
data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ?
{ "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
cursor = "action-attack-move";
}
else if (Engine.HotkeyIsPressed("session.patrol"))
{
data.command = "patrol";
data.targetClasses = { "attack": g_PatrolTargets };
cursor = "action-patrol";
}
return {
"possible": true,
"data": data,
"cursor": cursor
};
}
return {
"possible": ["move", "attack-move", "remove-guard", "patrol"].indexOf(action) != -1
};
}
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
- let targetState = GetExtendedEntityState(target);
+ let targetState = GetEntityState(target);
// Check if any entities in the selection can do some of the available actions with target
for (let entityID of selection)
{
- let entState = GetExtendedEntityState(entityID);
+ let entState = GetEntityState(entityID);
if (!entState)
continue;
if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
{
let r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
if (r && r.possible) // return true if it's possible for one of the entities
return r;
}
}
return { "possible": false };
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20874)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20875)
@@ -1,2034 +1,1996 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised for the biggest part
// So most of the attributes shouldn't be serialized
// Return an object with a small selection of deterministic data
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out what phase we are in
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
// store player ally/neutral/enemy data as arrays
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
// Add timeElapsed
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
// Add ceasefire info
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
// Add cinema path info
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
// Add the game type and allied victory
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.gameType = cmpEndGameManager.GetGameType();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
// Add basic statistics to each player
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
// Get basic simulation info
let ret = this.GetSimulationState();
// Add statistics to each player
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
- else
- return this.renamedEntities;
+
+ return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"template": template,
"alertRaiser": null,
+ "armour": null,
+ "attack": null,
"builder": null,
+ "buildingAI": null,
+ "buildRate": null,
+ "buildTime": null,
"canGarrison": null,
+ "deathDamage": null,
+ "heal": null,
"identity": null,
+ "isBarterMarket": null,
"fogging": null,
"foundation": null,
"garrisonHolder": null,
"gate": null,
"guard": null,
+ "loot": null,
"market": null,
"mirage": null,
"pack": null,
+ "promotion": null,
"upgrade" : null,
"player": -1,
"position": null,
"production": null,
"rallyPoint": null,
+ "repairRate": null,
"resourceCarrying": null,
+ "resourceDropsite": null,
+ "resourceGatherRates": null,
+ "resourceSupply": null,
+ "resourceTrickle": null,
"rotation": null,
+ "speed": null,
"trader": null,
+ "turretParent":null,
"unitAI": null,
"visibility": null,
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
ret.rotation = cmpPosition.GetRotation();
}
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval"),
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress(),
};
var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades" : cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo()
};
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
if (cmpFogging)
ret.fogging = {
"mirage": cmpFogging.IsMiraged(player) ? cmpFogging.GetMirage(player) : null
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
+ {
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage(),
"numBuilders": cmpFoundation.GetNumBuilders()
};
+ ret.buildRate = cmpFoundation.GetBuildRate();
+ ret.buildTime = cmpFoundation.GetBuildTime();
+ }
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
+ {
ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() };
+ ret.repairRate = cmpRepairable.GetRepairRate();
+ }
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"possibleStances": cmpUnitAI.GetPossibleStances(),
"isIdle":cmpUnitAI.IsIdle(),
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
+ {
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
+ ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
+ }
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked(),
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = {
"level": cmpAlertRaiser.GetLevel(),
"canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
"hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
- return ret;
-};
-
-/**
- * Get additionnal entity info, rarely used in the gui
- */
-GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
-{
- let ret = {
- "armour": null,
- "attack": null,
- "buildingAI": null,
- "deathDamage": null,
- "heal": null,
- "isBarterMarket": null,
- "loot": null,
- "obstruction": null,
- "turretParent":null,
- "promotion": null,
- "repairRate": null,
- "buildRate": null,
- "buildTime": null,
- "resourceDropsite": null,
- "resourceGatherRates": null,
- "resourceSupply": null,
- "resourceTrickle": null,
- "speed": null,
- };
-
- let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
-
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = cmpAttack.GetAttackStrengths(type);
ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// not a ranged attack, set some defaults
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
- let cmpPosition = Engine.QueryInterface(ent, IID_Position);
- let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
- let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
-
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the range in front of it, no spread. So angle = 0
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths();
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras)
ret.auras = cmpAuras.GetDescriptions();
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
let cmpDeathDamage = Engine.QueryInterface(ent, IID_DeathDamage);
if (cmpDeathDamage)
ret.deathDamage = cmpDeathDamage.GetDeathDamageStrengths();
- let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
- if (cmpObstruction)
- ret.obstruction = {
- "controlGroup": cmpObstruction.GetControlGroup(),
- "controlGroup2": cmpObstruction.GetControlGroup2(),
- };
-
- let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
- let cmpRepairable = Engine.QueryInterface(ent, IID_Repairable);
- if (cmpRepairable)
- ret.repairRate = cmpRepairable.GetRepairRate();
-
- let cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
- if (cmpFoundation)
- {
- ret.buildRate = cmpFoundation.GetBuildRate();
- ret.buildTime = cmpFoundation.GetBuildTime();
- }
-
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
- let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
- if (cmpResourceGatherer)
- ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
-
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"hp": cmpHeal.GetHP(),
"range": cmpHeal.GetRange().max,
"rate": cmpHeal.GetRate(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
- let resources = cmpLoot.GetResources();
- ret.loot = {
- "xp": cmpLoot.GetXp()
- };
- for (let res of Resources.GetCodes())
- ret.loot[res] = resources[res];
+ ret.loot = cmpLoot.GetResources();
+ ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
- {
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetTimer(),
- "rates": {}
+ "rates": cmpResourceTrickle.GetRates()
};
- let rates = cmpResourceTrickle.GetRates();
- for (let res in rates)
- ret.resourceTrickle.rates[res] = rates[res];
- }
-
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetRunSpeed()
};
return ret;
};
+GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
+{
+ return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
+};
+
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
-GuiInterface.prototype.GetTemplateData = function(player, name)
+GuiInterface.prototype.GetTemplateData = function(player, templateName)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
- let template = cmpTemplateManager.GetTemplate(name);
+ let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
aurasTemplate[name] = AuraTemplates.Get(name);
return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
// Returns technologies that are being actively researched, along with
// which entity is researching them and how far along the research is.
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
else
ret[tech].progress = 0;
}
return ret;
};
// Returns the battle state of the player.
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, data)
{
return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// filter on players and time, since the delete timer might be executed with a delay
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
// GetLastFormationName is named in a strange way as it (also) is
// the value of the current formation (see Formation.js LoadFormation)
if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
return true;
}
return false;
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
/**
* Updates player colors on the minimap.
*/
GuiInterface.prototype.UpdateDisplayedPlayerColors = function()
{
for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetGaiaAndNonGaiaEntities())
{
let cmpMinimap = Engine.QueryInterface(ent, IID_Minimap);
if (cmpMinimap)
cmpMinimap.UpdateColor();
}
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
let playerColors = {}; // cache of owner -> color map
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color:
let owner = -1;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r":1, "g":1, "b":1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != -1)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location)
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": [],
};
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
// Move the preview into the right location
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// do some entity cache management and check for snapping
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// we're clearing the preview, clear the entity cache and bail
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// keep template data around
}
return false;
}
- else
+
+ // Move all existing cached entities outside of the world and reset their use count
+ for (let tpl in this.placementWallEntities)
{
- // Move all existing cached entities outside of the world and reset their use count
- for (let tpl in this.placementWallEntities)
+ for (let ent of this.placementWallEntities[tpl].entities)
{
- for (let ent of this.placementWallEntities[tpl].entities)
- {
- let pos = Engine.QueryInterface(ent, IID_Position);
- if (pos)
- pos.MoveOutOfWorld();
- }
-
- this.placementWallEntities[tpl].numUsed = 0;
+ let pos = Engine.QueryInterface(ent, IID_Position);
+ if (pos)
+ pos.MoveOutOfWorld();
}
- // Create cache entries for templates we haven't seen before
- for (let type in wallSet.templates)
+ this.placementWallEntities[tpl].numUsed = 0;
+ }
+
+ // Create cache entries for templates we haven't seen before
+ for (let type in wallSet.templates)
+ {
+ if (type == "curves")
+ continue;
+
+ let tpl = wallSet.templates[type];
+ if (!(tpl in this.placementWallEntities))
{
- if (type == "curves")
- continue;
+ this.placementWallEntities[tpl] = {
+ "numUsed": 0,
+ "entities": [],
+ "templateData": this.GetTemplateData(player, tpl),
+ };
- let tpl = wallSet.templates[type];
- if (!(tpl in this.placementWallEntities))
+ // ensure that the loaded template data contains a wallPiece component
+ if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
- this.placementWallEntities[tpl] = {
- "numUsed": 0,
- "entities": [],
- "templateData": this.GetTemplateData(player, tpl),
- };
-
- // ensure that the loaded template data contains a wallPiece component
- if (!this.placementWallEntities[tpl].templateData.wallPiece)
- {
- error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
- return false;
- }
+ error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
+ return false;
}
}
}
// prevent division by zero errors further on if the start and end positions are the same
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// clear the single-building preview entity (we'll be rolling our own)
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// calculate wall placement and position preview entities
let result = {
"pieces": [],
"cost": { "population": 0, "populationBonus": 0, "time": 0 },
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length > 0 && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
- "controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)],
+ "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true, // preview only, must not appear in the result
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
- "angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
+ "angle": previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
- previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []);
+ previewEntities[previewEntities.length-1].controlGroups = previewEntities[previewEntities.length-1].controlGroups || [];
previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
- "controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)],
+ "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
- "angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
+ "angle": previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
// reuse an existing one
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// move piece to right location
// TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
// check whether this wall piece can be validly positioned here
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region
// TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
- let visible = (cmpRangeManager.GetLosVisibility(ent, player) != "hidden");
+ let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement
- validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success);
+ validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
// (TODO: break unlikely ties by choosing the lowest entity ID)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
var bucket = filtered.bucket;
if(bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if(!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
{
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
}
else if (!firstMarket)
{
result = { "type": "set first" };
}
else if (!secondMarket)
{
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
}
else
{
// Else both markets are not null and target is different from them
result = { "type": "set first" };
}
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
return QueryPlayerIDInterface(player).GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
- "GetExtendedEntityState": 1,
+ "GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
- else
- throw new Error("Invalid GuiInterface Call name \""+name+"\"");
+
+ throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 20874)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 20875)
@@ -1,639 +1,625 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/RallyPoint.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
GetPrices: function() {
return {
"buy": { "food": 150 },
"sell": { "food": 25 }
};
},
PlayerHasMarket: function () { return false; }
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
GetGameType: function() { return "conquest"; },
GetAlliedVictory: function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetNumPlayers: function() { return 2; },
GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; }
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
GetLosVisibility: function(ent, player) { return "visible"; },
GetLosCircular: function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
GetCurrentTemplateName: function(ent) { return "example"; },
GetTemplate: function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
GetTime: function() { return 0; },
SetTimeout: function(ent, iid, funcname, time, data) { return 0; }
});
AddMock(100, IID_Player, {
GetName: function() { return "Player 1"; },
GetCiv: function() { return "gaia"; },
GetColor: function() { return { r: 1, g: 1, b: 1, a: 1}; },
CanControlAllUnits: function() { return false; },
GetPopulationCount: function() { return 10; },
GetPopulationLimit: function() { return 20; },
GetMaxPopulation: function() { return 200; },
GetResourceCounts: function() { return { food: 100 }; },
GetPanelEntities: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() { return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
IsAlly: function() { return false; },
IsMutualAlly: function() { return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return true; },
GetDisabledTemplates: function() { return {}; },
GetDisabledTechnologies: function() { return {}; },
GetSpyCostMultiplier: function() { return 1; },
HasSharedDropsites: function() { return false; },
HasSharedLos: function() { return false; }
});
AddMock(100, IID_EntityLimits, {
GetLimits: function() { return {"Foo": 10}; },
GetCounts: function() { return {"Foo": 5}; },
GetLimitChangers: function() {return {"Foo": {}}; }
});
AddMock(100, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(100, IID_StatisticsTracker, {
GetBasicStatistics: function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
GetSequences: function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; }
});
AddMock(101, IID_Player, {
GetName: function() { return "Player 2"; },
GetCiv: function() { return "mace"; },
GetColor: function() { return { r: 1, g: 0, b: 0, a: 1}; },
CanControlAllUnits: function() { return true; },
GetPopulationCount: function() { return 40; },
GetPopulationLimit: function() { return 30; },
GetMaxPopulation: function() { return 300; },
GetResourceCounts: function() { return { food: 200 }; },
GetPanelEntities: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() {return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
IsAlly: function() { return true; },
IsMutualAlly: function() {return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return false; },
GetDisabledTemplates: function() { return {}; },
GetDisabledTechnologies: function() { return {}; },
GetSpyCostMultiplier: function() { return 1; },
HasSharedDropsites: function() { return false; },
HasSharedLos: function() { return false; }
});
AddMock(101, IID_EntityLimits, {
GetLimits: function() { return {"Bar": 20}; },
GetCounts: function() { return {"Bar": 0}; },
GetLimitChangers: function() {return {"Bar": {}}; }
});
AddMock(101, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(101, IID_StatisticsTracker, {
GetBasicStatistics: function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
GetSequences: function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; }
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
players: [
{
name: "Player 1",
civ: "gaia",
color: { r:1, g:1, b:1, a:1 },
controlsAll: false,
popCount: 10,
popLimit: 20,
popMax: 200,
panelEntities: [],
resourceCounts: { food: 100 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
disabledTemplates: {},
disabledTechnologies: {},
hasSharedDropsites: false,
hasSharedLos: false,
spyCostMultiplier: 1,
phase: "village",
isAlly: [false, false],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [true, true],
entityLimits: {"Foo": 10},
entityCounts: {"Foo": 5},
entityLimitChangers: {"Foo": {}},
researchQueued: new Map(),
researchStarted: new Set(),
researchedTechs: new Set(),
classCounts: {},
typeCountsByClass: {},
canBarter: false,
barterPrices: {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
statistics: {
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0
},
percentMapExplored: 10
}
},
{
name: "Player 2",
civ: "mace",
color: { r:1, g:0, b:0, a:1 },
controlsAll: true,
popCount: 40,
popLimit: 30,
popMax: 300,
panelEntities: [],
resourceCounts: { food: 200 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
disabledTemplates: {},
disabledTechnologies: {},
hasSharedDropsites: false,
hasSharedLos: false,
spyCostMultiplier: 1,
phase: "village",
isAlly: [true, true],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [false, false],
entityLimits: {"Bar": 20},
entityCounts: {"Bar": 0},
entityLimitChangers: {"Bar": {}},
researchQueued: new Map(),
researchStarted: new Set(),
researchedTechs: new Set(),
classCounts: {},
typeCountsByClass: {},
canBarter: false,
barterPrices: {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
statistics: {
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0
},
percentMapExplored: 10
}
}
],
circularMap: false,
timeElapsed: 0,
gameType: "conquest",
alliedVictory: false
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r":1, "g":1, "b":1, "a":1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": {"Foo": 10},
"entityCounts": {"Foo": 5},
"entityLimitChangers": {"Foo": {}},
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r":1, "g":0, "b":0, "a":1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": {"Bar": 20},
"entityCounts": {"Bar": 0},
"entityLimitChangers": {"Bar": {}},
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
}
],
"circularMap": false,
"timeElapsed": 0,
"gameType": "conquest",
"alliedVictory": false
});
AddMock(10, IID_Builder, {
GetEntitiesList: function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
GetHitpoints: function() { return 50; },
GetMaxHitpoints: function() { return 60; },
IsRepairable: function() { return false; },
IsUnhealable: function() { return false; }
});
AddMock(10, IID_Identity, {
GetClassesList: function() { return ["class1", "class2"]; },
GetVisibleClassesList: function() { return ["class3", "class4"]; },
GetRank: function() { return "foo"; },
GetSelectionGroupName: function() { return "Selection Group Name"; },
HasClass: function() { return true; },
IsUndeletable: function() { return false; }
});
AddMock(10, IID_Position, {
GetTurretParent: function() {return INVALID_ENTITY;},
GetPosition: function() {
return {x:1, y:2, z:3};
},
GetRotation: function() {
return {x:4, y:5, z:6};
},
IsInWorld: function() {
return true;
}
});
AddMock(10, IID_ResourceTrickle, {
"GetTimer": () => 1250,
- "GetRates": () => ({
- "food": 2,
- "wood": 3,
- "stone": 5,
- "metal": 9
- })
+ "GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
- id: 10,
- template: "example",
- alertRaiser: null,
- builder: true,
- canGarrison: false,
- identity: {
- rank: "foo",
- classes: ["class1", "class2"],
- visibleClasses: ["class3", "class4"],
- selectionGroupName: "Selection Group Name",
- canDelete: true
+ "id": 10,
+ "template": "example",
+ "alertRaiser": null,
+ "armour": null,
+ "attack": null,
+ "builder": true,
+ "buildingAI": null,
+ "buildRate": null,
+ "buildTime": null,
+ "canGarrison": false,
+ "deathDamage": null,
+ "heal": null,
+ "identity": {
+ "rank": "foo",
+ "classes": ["class1", "class2"],
+ "visibleClasses": ["class3", "class4"],
+ "selectionGroupName": "Selection Group Name",
+ "canDelete": true
},
- fogging: null,
- foundation: null,
- garrisonHolder: null,
- gate: null,
- guard: null,
- market: null,
- mirage: null,
- pack: null,
- upgrade: null,
- player: -1,
- position: {x:1, y:2, z:3},
- production: null,
- rallyPoint: null,
- resourceCarrying: null,
- rotation: {x:4, y:5, z:6},
- trader: null,
- unitAI: null,
- visibility: "visible",
- hitpoints: 50,
- maxHitpoints: 60,
- needsRepair: false,
- needsHeal: true
-});
-
-TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedEntityState(-1, 10), {
- armour: null,
- attack: null,
- buildingAI: null,
- deathDamage:null,
- heal: null,
- isBarterMarket: true,
- loot: null,
- obstruction: null,
- turretParent: null,
- promotion: null,
- repairRate: null,
- buildRate: null,
- buildTime: null,
- resourceDropsite: null,
- resourceGatherRates: null,
- resourceSupply: null,
- resourceTrickle: {
+ "isBarterMarket": true,
+ "fogging": null,
+ "foundation": null,
+ "garrisonHolder": null,
+ "gate": null,
+ "guard": null,
+ "loot": null,
+ "market": null,
+ "mirage": null,
+ "pack": null,
+ "promotion": null,
+ "upgrade" : null,
+ "player": -1,
+ "position": {x:1, y:2, z:3},
+ "production": null,
+ "rallyPoint": null,
+ "repairRate": null,
+ "resourceCarrying": null,
+ "resourceDropsite": null,
+ "resourceGatherRates": null,
+ "resourceSupply": null,
+ "resourceTrickle": {
"interval": 1250,
- "rates": {
- "food": 2,
- "wood": 3,
- "stone": 5,
- "metal": 9
- }
+ "rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
},
- speed: null
+ "rotation": {x:4, y:5, z:6},
+ "speed": null,
+ "trader": null,
+ "turretParent":null,
+ "unitAI": null,
+ "visibility": "visible",
+ "hitpoints": 50,
+ "maxHitpoints": 60,
+ "needsRepair": false,
+ "needsHeal": true
});