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 @@ closeOpenDialogs(); openChat(); openChat(g_IsObserver ? "/observers" : "/allies"); openChat(g_LastChatAddressee); g_ShowGUI = !g_ShowGUI; toggleMenu(); toggleTrade(); toggleTutorial(); openGameSummary(); toggleConfigBool("silhouettes"); var newSetting = !Engine.Renderer_GetShowSkyEnabled(); Engine.Renderer_SetShowSkyEnabled(newSetting); togglePause(); Engine.QuickSave(); Engine.QuickLoad(); - performCommand(g_Selection.toList().map(ent => GetExtendedEntityState(ent)), "delete"); + performCommand(g_Selection.toList().map(ent => GetEntityState(ent)), "delete"); unloadAll(); stopUnits(g_Selection.toList()); backToWork(); updateSelectionDetails(); updateSelectionDetails(); updateSelectionDetails(); updateBarterButtons(); updateSelectionDetails(); updateBarterButtons(); findIdleUnit(g_MilitaryTypes); findIdleUnit(["!Domestic"]); clearSelection(); toggleRangeOverlay("Attack"); toggleRangeOverlay("Auras"); toggleRangeOverlay("Heal"); g_ShowAllStatusBars = !g_ShowAllStatusBars; recalculateStatusBarDisplay(); 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 });