Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -453,6 +453,7 @@ pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification [gui.session.notifications] +discovered = true ; Show a chat notification if you discovered a player (structure) for the first time attack = true ; Show a chat notification if you are attacked by another player tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode barter = true ; Show a chat notification to observers when a player bartered resources Index: binaries/data/mods/public/audio/interface/alarm/alarm_discovered_player.xml =================================================================== --- binaries/data/mods/public/audio/interface/alarm/alarm_discovered_player.xml +++ binaries/data/mods/public/audio/interface/alarm/alarm_discovered_player.xml @@ -0,0 +1,17 @@ + + + 1 + owner + 0.35 + 100 + 1 + 0 + 1 + 1 + 0.35 + 0.30 + 1 + 1 + audio/interface/alarm/ + alarm_discovered_player.ogg + Index: binaries/data/mods/public/gui/options/options.json =================================================================== --- binaries/data/mods/public/gui/options/options.json +++ binaries/data/mods/public/gui/options/options.json @@ -630,6 +630,12 @@ }, { "type": "boolean", + "label": "Chat notification discovered", + "tooltip": "Show a chat notification if you spotted a player (structure).", + "config": "gui.session.notifications.discovered" + }, + { + "type": "boolean", "label": "Chat notification tribute", "tooltip": "Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode.", "config": "gui.session.notifications.tribute" Index: binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.js =================================================================== --- binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.js +++ binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.js @@ -3,16 +3,22 @@ constructor(civData) { this.handlers = new Set(); + const playerID = Engine.GetPlayerID(); + this.hasViewPermission = Engine.GuiInterfaceCall("HasSpyTech", { "player": playerID }) || Engine.GuiInterfaceCall("GetState", { "player": playerID }) != "active"; + warn(`hasSpyTech = ${ Engine.GuiInterfaceCall("HasSpyTech", { "player": playerID }) } isObserver = ${ Engine.GuiInterfaceCall("GetState", { "player": playerID }) != "active" }`); + let civList = Object.keys(civData).map(civ => ({ "name": civData[civ].Name, "code": civ, })).sort(sortNameIgnoreCase); - this.civSelectionHeading = Engine.GetGUIObjectByName("civSelectionHeading"); - this.civSelectionHeading.caption = this.Caption; + const defaultcivSelectionHeadingSize = this.civSelectionHeading["size"]; + this.civSelectionHeading.caption = this.hasViewPermission ? this.Caption : translate("Espionage tech (civic center) is required to view other civilizations"); + this.civSelectionHeading["size"] = this.hasViewPermission ? defaultcivSelectionHeadingSize : "0 10 100% 48"; this.civSelection = Engine.GetGUIObjectByName("civSelection"); + this.civSelection.hidden = !this.hasViewPermission; this.civSelection.list = civList.map(c => c.name); this.civSelection.list_data = civList.map(c => c.code); this.civSelection.onSelectionChange = () => this.onSelectionChange(this); Index: binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js =================================================================== --- binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js +++ binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js @@ -29,6 +29,29 @@ } }; +ChatMessageFormatSimulation.discovered = class +{ + parse(msg) + { + if (msg.player != g_ViewedPlayer || Engine.ConfigDB_GetValue("user", "gui.session.notifications.discovered") != "true") + return ""; + + const message = translate("%(icon)s %(playerFound)s (%(diplomacy)s) has been spotted!"); + + return { + "text": sprintf(message, { + "icon": '[icon="icon_alert"]', + "playerFound": colorizePlayernameByID(msg.playerFound), + "diplomacy": msg.diplomacy + }), + "callback": ((target, position) => function() { + focusAttack({ "target": target, "position": position }); + })(msg.target, msg.position), + "tooltip": translate("Click to focus location.") + }; + } +}; + ChatMessageFormatSimulation.barter = class { parse(msg) Index: binaries/data/mods/public/gui/session/diplomacy/playercontrols/DiplomacyPlayerText.js =================================================================== --- binaries/data/mods/public/gui/session/diplomacy/playercontrols/DiplomacyPlayerText.js +++ binaries/data/mods/public/gui/session/diplomacy/playercontrols/DiplomacyPlayerText.js @@ -15,6 +15,8 @@ this.diplomacyPlayerTeam = Engine.GetGUIObjectByName("diplomacyPlayerTeam" + id); this.diplomacyPlayerTheirs = Engine.GetGUIObjectByName("diplomacyPlayerTheirs" + id); this.diplomacyPlayerOutcome = Engine.GetGUIObjectByName("diplomacyPlayerOutcome" + id); + this.seenPlayers = Engine.GuiInterfaceCall("GetSeenPlayers", { "player": g_ViewedPlayer }); + this.HasEpionageTech = Engine.GuiInterfaceCall("HasSpyTech", { "player": g_ViewedPlayer }); this.init(); } @@ -25,9 +27,11 @@ if (Engine.IsAtlasRunning()) return; - this.diplomacyPlayerCiv.caption = g_CivData[g_Players[this.playerID].civ].Name; + this.diplomacyPlayerCiv.caption = this.HasEpionageTech ? g_CivData[g_Players[this.playerID].civ].Name : "?"; this.diplomacyPlayerName.tooltip = translateAISettings(g_InitAttributes.settings.PlayerData[this.playerID]); + this.diplomacyPlayerName.caption = g_Players[this.playerID].name; + // Apply offset let rowSize = DiplomacyDialogPlayerControl.prototype.DiplomacyPlayerText.getRowHeight(); let size = this.diplomacyPlayer.size; @@ -39,24 +43,46 @@ update() { + + setOutcomeIcon(g_Players[this.playerID].state, this.diplomacyPlayerOutcome); - this.diplomacyPlayer.sprite = "color:" + g_DiplomacyColors.getPlayerColor(this.playerID, 32); + this.seenPlayers = Engine.GuiInterfaceCall("GetSeenPlayers", { "player": g_ViewedPlayer }); + this.HasEpionageTech = Engine.GuiInterfaceCall("HasSpyTech", { "player": g_ViewedPlayer }); - this.diplomacyPlayerName.caption = colorizePlayernameByID(this.playerID); + warn(uneval(`DIPLOMACY VIEW PERMISSION `)); - this.diplomacyPlayerTeam.caption = + if (this.HasEpionageTech || this.playerID == g_ViewedPlayer || g_Players[this.playerID].isAlly[g_ViewedPlayer]) + { + this.diplomacyPlayer.sprite = "color:" + g_DiplomacyColors.getPlayerColor(this.playerID, 32); + this.diplomacyPlayerName.caption = colorizePlayernameByID(this.playerID); + this.diplomacyPlayerCiv.caption = g_CivData[g_Players[this.playerID].civ].Name; + + this.diplomacyPlayerTeam.caption = g_Players[this.playerID].team >= 0 ? g_Players[this.playerID].team + 1 : translateWithContext("team", this.NoTeam); - this.diplomacyPlayerTheirs.caption = - this.playerID == g_ViewedPlayer ? "" : - g_Players[this.playerID].isAlly[g_ViewedPlayer] ? - translate(this.Ally) : - g_Players[this.playerID].isNeutral[g_ViewedPlayer] ? - translate(this.Neutral) : - translate(this.Enemy); + this.diplomacyPlayerTheirs.caption = + this.playerID == g_ViewedPlayer ? "" : + g_Players[this.playerID].isAlly[g_ViewedPlayer] ? + translate(this.Ally) : + g_Players[this.playerID].isNeutral[g_ViewedPlayer] ? + translate(this.Neutral) : + translate(this.Enemy); + } + else if (this.seenPlayers.includes(this.playerID)) + { + this.diplomacyPlayer.sprite = "color:" + g_DiplomacyColors.getPlayerColor(this.playerID, 32); + this.diplomacyPlayerName.caption = colorizePlayernameByID(this.playerID); + this.diplomacyPlayerCiv.caption = g_CivData[g_Players[this.playerID].civ].Name; + } + else + { + this.diplomacyPlayerTeam.caption = "?"; + this.diplomacyPlayerTheirs.caption = "?"; + } + } }; Index: binaries/data/mods/public/gui/session/messages.js =================================================================== --- binaries/data/mods/public/gui/session/messages.js +++ binaries/data/mods/public/gui/session/messages.js @@ -222,6 +222,36 @@ "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, + "discovered": function(notification, player) + { + if (player != g_ViewedPlayer) + return; + + // Focus camera on attacks + if (g_FollowPlayer) + { + setCameraFollow(notification.target); + + g_Selection.reset(); + if (notification.target) + g_Selection.addList([notification.target]); + } + + g_LastAttack = { "target": notification.target, "position": notification.position }; + + if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.discovered") !== "true") + return; + + addChatMessage({ + "type": "discovered", + "player": player, + "playerFound": notification.playerFound, + "target": notification.target, + "position": notification.position, + "diplomacy": notification.diplomacy + }); + + }, "phase": function(notification, player) { addChatMessage({ Index: binaries/data/mods/public/gui/session/selection_details.js =================================================================== --- binaries/data/mods/public/gui/session/selection_details.js +++ binaries/data/mods/public/gui/session/selection_details.js @@ -48,15 +48,54 @@ } } -// Fills out information that most entities have +/** + * @param {object} enstState - used to check the entity player id and entity type + * @returns {boolean} + * Check if the player has permission to view unit statistics. + * Returns true if the player has the Espionage tech, the entity is Gaia, Allied or a Resource. + * Or if the player is an observer or anything else but an active state. + */ +function CheckViewPermission(entState) +{ + const playerID = Engine.GetPlayerID(); + const playerState = Engine.GuiInterfaceCall("GetState", { "player": playerID }); + + // observers + if (playerID == -1 || playerState != 'active') + { + warn(`SELECTION VIEW PERMISSION ${ playerID == -1 || playerState != 'active' } `); + return true; + } + + const entityPlayerID = entState.player; + const technologyEnabled = Engine.GuiInterfaceCall("HasSpyTech", { "player": playerID }); + + warn(`SELECTION VIEW PERMISSION ${ (technologyEnabled || Engine.GuiInterfaceCall("GetState", { "player": playerID }) != "active" || g_Players[entityPlayerID].isAlly[playerID] || playerID == entityPlayerID || !!entState.resourceSupply) } `); + + // If the player has the tech no need to check further, full permission granted. + if (technologyEnabled) + return true; + + + if (g_Players[entityPlayerID].isAlly[playerID] || playerID == entityPlayerID || !!entState.resourceSupply) + return true; + + return false; +} + +// Fills out information that most entities have, unless the CheckViewPermission() returns false. function displaySingle(entState) { + const hasViewPermission = CheckViewPermission(entState); + let template = GetTemplateData(entState.template); - let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic; + let primaryName = hasViewPermission ? g_SpecificNamesPrimary ? template.name.specific : template.name.generic + : sprintf(translate("Espionage tech required")); let secondaryName; if (g_ShowSecondaryNames) - secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific; + secondaryName = hasViewPermission ? g_SpecificNamesPrimary ? template.name.generic : template.name.specific + : sprintf(translate("can be researched at the civic center")); // If packed, add that to the generic name (reduces template clutter). if (template.pack && template.pack.state == "packed") @@ -71,14 +110,14 @@ let civName = g_CivData[playerState.civ].Name; let civEmblem = g_CivData[playerState.civ].Emblem; - let playerName = playerState.name; + let playerName = hasViewPermission ? playerState.name : "?"; // 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) + if (entState.identity && entState.identity.rank && entState.identity.classes && hasViewPermission) { const rankObj = GetTechnologyData(entState.identity.rankTechName, playerState.civ); Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), { @@ -136,11 +175,11 @@ { let unitHealthBar = Engine.GetGUIObjectByName("healthBar"); let healthSize = unitHealthBar.size; - healthSize.rright = 100 * Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints)); + healthSize.rright = hasViewPermission ? 100 * Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints)) : 100; unitHealthBar.size = healthSize; Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), { - "hitpoints": Math.ceil(entState.hitpoints), - "maxHitpoints": Math.ceil(entState.maxHitpoints) + "hitpoints": hasViewPermission ? Math.ceil(entState.hitpoints) : "?", + "maxHitpoints": hasViewPermission ? Math.ceil(entState.maxHitpoints) : "?" }); healthSection.size = sectionPosTop.size; @@ -190,7 +229,7 @@ } // Experience - Engine.GetGUIObjectByName("experience").hidden = !entState.promotion; + Engine.GetGUIObjectByName("experience").hidden = !entState.promotion || !hasViewPermission; if (entState.promotion) { let experienceBar = Engine.GetGUIObjectByName("experienceBar"); @@ -325,7 +364,7 @@ Engine.GetGUIObjectByName("icon").sprite = template.icon ? ("stretched:session/portraits/" + template.icon) : "BackgroundBlack"; if (template.icon) Engine.GetGUIObjectByName("iconBorder").onPressRight = () => { - showTemplateDetails(entState.template, playerState.civ); + hasViewPermission ? showTemplateDetails(entState.template, playerState.civ) : ''; }; let detailedTooltip = [ @@ -342,7 +381,7 @@ getUpkeepTooltip, getLootTooltip ].map(func => func(entState)).filter(tip => tip).join("\n"); - if (detailedTooltip) + if (detailedTooltip && hasViewPermission) { Engine.GetGUIObjectByName("attackAndResistanceStats").hidden = false; Engine.GetGUIObjectByName("attackAndResistanceStats").tooltip = detailedTooltip; @@ -353,13 +392,13 @@ let iconTooltips = []; iconTooltips.push(setStringTags(primaryName, g_TooltipTextFormats.namePrimaryBig)); - iconTooltips = iconTooltips.concat([ + iconTooltips = hasViewPermission ? iconTooltips.concat([ getVisibleEntityClassesFormatted, getAurasTooltip, getEntityTooltip, getTreasureTooltip, showTemplateViewerOnRightClickTooltip - ].map(func => func(template))); + ].map(func => func(template))) : iconTooltips.concat([].map(func => func(template))); Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltips.filter(tip => tip).join("\n"); Index: binaries/data/mods/public/gui/session/setup.xml =================================================================== --- binaries/data/mods/public/gui/session/setup.xml +++ binaries/data/mods/public/gui/session/setup.xml @@ -4,4 +4,8 @@ sprite="stretched:session/icons/focus-attacked.png" size="14 14" /> + Index: binaries/data/mods/public/simulation/components/Fogging.js =================================================================== --- binaries/data/mods/public/simulation/components/Fogging.js +++ binaries/data/mods/public/simulation/components/Fogging.js @@ -164,6 +164,15 @@ return this.seen[player]; }; + +/** + * @returns {array} - playerIDs seen done by exploring + */ +Fogging.prototype.GetSeenPlayers = function() +{ + return QueryOwnerInterface(this.entity).hasSeenPlayers; +}; + Fogging.prototype.OnOwnershipChanged = function(msg) { // Always activate fogging for non-Gaia entities. @@ -202,6 +211,11 @@ { this.miraged[msg.player] = false; this.seen[msg.player] = true; + + // add first contact with players to array and push a spotted player notification + const cmpPlayer = QueryPlayerIDInterface(msg.player); + if (cmpPlayer) + cmpPlayer.AddSeenPlayer(QueryOwnerInterface(msg.ent).GetPlayerID(), msg.ent); } if (msg.newVisibility == VIS_FOGGED && this.activated) Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -707,6 +707,36 @@ }; /** + * @returns {string} the 'active', 'defeated' or 'won' state of the player. + */ +GuiInterface.prototype.GetState = function(player) +{ + const cmpPlayer = QueryPlayerIDInterface(player, IID_Player); + if (!cmpPlayer) + return false; + + return cmpPlayer.GetState(); +}; + +/** + * @returns {array} - of all players seen by exploring (fogging) + */ +GuiInterface.prototype.GetSeenPlayers = function(player) +{ + const cmpPlayer = QueryPlayerIDInterface(player, IID_Player); + return cmpPlayer.GetSeenPlayers(); +}; + +/** + * @param {number} - playerID + * @returns {bool} - checks if the player has the spy tech + */ +GuiInterface.prototype.HasSpyTech = function(player) +{ + return QueryPlayerIDInterface(player).HasSpyTech(); +}; + +/** * Returns a list of ongoing attacks against the player. */ GuiInterface.prototype.GetIncomingAttacks = function(player) @@ -979,25 +1009,44 @@ return Array.from(this.entsWithAuraAndStatusBars); }; +GuiInterface.prototype.CheckStatusBarsViewPermission = function(player, ent) +{ + const entPlayer = QueryOwnerInterface(ent).GetPlayerID(); + const cmpPlayer = QueryPlayerIDInterface(player); + + const isResource = Engine.QueryInterface(ent, IID_ResourceSupply) ? true : false; + const isObserver = cmpPlayer ? false : true; + const HasSpyTech = cmpPlayer ? cmpPlayer.HasSpyTech() : false; + const isAlly = cmpPlayer ? cmpPlayer.IsAlly(entPlayer) : false; + + warn(uneval(`STATUSBAR VIEW PERMISSION ${ (player == entPlayer || HasSpyTech || isResource || isAlly || isObserver) } `)); + + if (player == entPlayer || HasSpyTech || isResource || isAlly || isObserver) + return true; + + return false; +}; + 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) + if (!cmpStatusBars || !this.CheckStatusBarsViewPermission(player, ent)) continue; + cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience); - let cmpAuras = Engine.QueryInterface(ent, IID_Auras); + const cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) continue; - for (let name of cmpAuras.GetAuraNames()) + for (const name of cmpAuras.GetAuraNames()) { if (!cmpAuras.GetOverlayIcon(name)) continue; - for (let e of cmpAuras.GetAffectedEntities(name)) + for (const e of cmpAuras.GetAffectedEntities(name)) affectedEnts.add(e); if (cmd.enabled) this.entsWithAuraAndStatusBars.add(ent); @@ -2080,6 +2129,9 @@ "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, + "GetState": 1, + "GetSeenPlayers": 1, + "HasSpyTech": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNotifications": 1, Index: binaries/data/mods/public/simulation/components/Player.js =================================================================== --- binaries/data/mods/public/simulation/components/Player.js +++ binaries/data/mods/public/simulation/components/Player.js @@ -56,6 +56,7 @@ Player.prototype.Init = function() { this.playerID = undefined; + this.hasSeenPlayers = []; this.color = undefined; this.diplomacyColor = undefined; this.displayDiplomacyColor = false; @@ -78,6 +79,7 @@ this.cheatsEnabled = false; this.panelEntities = []; this.resourceNames = {}; + this.hasSpyTech = false; this.disabledTemplates = {}; this.disabledTechnologies = {}; this.spyCostMultiplier = +this.template.SpyCostMultiplier; @@ -116,6 +118,55 @@ return this.playerID; }; +/** + * Check if a player has been seen before, excluding gaia. + */ +Player.prototype.HasSeenPlayer = function(player) +{ + if (!this.GetSeenPlayers().includes(player) && player != this.GetPlayerID() && player != 0) + return false; + + return true; +}; + +/** + * Add a seen player to the array and push a notification with sound about it with a location. + * A player is count as seen if you found an enemy structure. + * @param {number} player - needed for diplomacy + * @param {number} ent - needed for pushing notification of the spotted entity location + */ +Player.prototype.AddSeenPlayer = function(player, ent) +{ + if (this.HasSeenPlayer(player)) + return; + + const diplomacy = this.IsAlly(player) ? "Allied" : this.IsNeutral(player) ? "Neutral" : "Enemy"; + + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ + "type": "discovered", + "target": ent, + "players": [this.GetPlayerID()], + "playerFound": player, + "position": Engine.QueryInterface(ent, IID_Position).GetPosition(), + "diplomacy": diplomacy + }); + + this.hasSeenPlayers.push(player); + Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer("interface/alarm/alarm_discovered_player.xml", this.GetPlayerID()); + warn(uneval(`DETECTED NEW PLAYER `)); + +}; + +Player.prototype.GetSeenPlayers = function() +{ + return this.hasSeenPlayers; +}; + +Player.prototype.HasSpyTech = function() +{ + return this.hasSpyTech; +}; + Player.prototype.SetColor = function(r, g, b) { let colorInitialized = !!this.color; @@ -807,6 +858,8 @@ this.UpdateSharedLos(); else if (msg.tech == this.template.SharedDropsitesTech) this.sharedDropsites = true; + else if (msg.tech == "unlock_spies") + this.hasSpyTech = true; }; Player.prototype.OnDiplomacyChanged = function() Index: binaries/data/mods/public/simulation/data/technologies/unlock_spies.json =================================================================== --- binaries/data/mods/public/simulation/data/technologies/unlock_spies.json +++ binaries/data/mods/public/simulation/data/technologies/unlock_spies.json @@ -1,6 +1,6 @@ { "genericName": "Espionage", - "description": "Merchants' first goal was trading, but they also gathered information about the countries they crossed.", + "description": "Send spies to gather information about your enemy's infastructure and army. Providing you useful statistics about enemy units and structures", "cost": { "food": 500, "metal": 500 @@ -9,6 +9,6 @@ "requirementsTooltip": "Unlocked in City Phase.", "icon": "spy_trader.png", "researchTime": 60, - "tooltip": "Allows bribing the units of other players in order to share their vision.", + "tooltip": "Unlocks viewing enemy unit statistics and enemy diplomacy info.", "soundComplete": "interface/alarm/alarm_upgradearmory.xml" }