diff --git a/binaries/data/mods/public/maps/scripts/TriggerHelper.js b/binaries/data/mods/public/maps/scripts/TriggerHelper.js index 2e01c129e0..abcd280c0b 100644 --- a/binaries/data/mods/public/maps/scripts/TriggerHelper.js +++ b/binaries/data/mods/public/maps/scripts/TriggerHelper.js @@ -603,4 +603,120 @@ TriggerHelper.SpawnAndTurretAtClasses = function(playerID, classes, templates, c return results; }; +/** + * Makes an entity permanently visible to all players by: + * - Setting alwaysVisible flag + * - Deactivating fogging (no mirages) + * - Exploring the surrounding area + * - Forcing a visibility update + * + * @param {number} ent - Entity ID to make permanently visible + */ +TriggerHelper.MakeEntityPermanentlyVisible = function(ent, exploreRadius = 50) +{ + const cmpVisibility = Engine.QueryInterface(ent, IID_Visibility); + if (cmpVisibility) + { + cmpVisibility.alwaysVisible = true; + cmpVisibility.SetActivated(true); + } + + const cmpFogging = Engine.QueryInterface(ent, IID_Fogging); + if (cmpFogging) + cmpFogging.PermanentlyReveal("all"); + + const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + const cmpPosition = Engine.QueryInterface(ent, IID_Position); + if (cmpRangeManager && cmpPosition && cmpPosition.IsInWorld()) + { + const pos = cmpPosition.GetPosition2D(); + const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + for (let i = 1; i < numPlayers; ++i) + cmpRangeManager.ExploreCircle(i, pos.x, pos.y, exploreRadius); + + cmpRangeManager.RequestVisibilityUpdate(ent); + } +}; + +/** + * Sends a notification to specific players. + * + * @param {string} message - The message to display (use markForTranslation) + * @param {number[]} players - Array of player IDs to send the notification to + * @param {number} duration - Duration in milliseconds + * @param {Object} parameters - Translation parameters (optional) + */ +TriggerHelper.SendNotification = function(message, players, duration = 30000, parameters = {}, translateParameters = []) +{ + const cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + if (!cmpGuiInterface) + return; + + // Handle case where message is an object from markForPluralTranslation + const messageStr = typeof message === "string" ? message : (message.message || String(message)); + + // Ensure all parameter values are primitive types + const safeParameters = {}; + for (const key in parameters) + { + const value = parameters[key]; + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") + safeParameters[key] = value; + else + safeParameters[key] = String(value); + } + + cmpGuiInterface.AddTimeNotification( + { + "message": messageStr, + "players": players, + "parameters": safeParameters, + "translateMessage": true, + "translateParameters": translateParameters + }, + duration + ); +}; + +/** + * Sends a notification to a specific player and a different notification to all other active players. + * + * @param {number} player - The player ID who gets the 'owner' message + * @param {string} ownerMessage - Message for the specified player (use markForTranslation) + * @param {string} othersMessage - Message for all other active players (use markForTranslation) + * @param {number} duration - Duration in milliseconds + * @param {Object} parameters - Translation parameters shared by both messages (optional) + * @param {Object} ownerParameters - Additional parameters for the owner message (optional) + * @param {Object} othersParameters - Additional parameters for the others message (optional) + */ +TriggerHelper.SendDualNotification = function(player, ownerMessage, othersMessage, duration = 30000, parameters = {}, ownerParameters = {}, othersParameters = {}) +{ + const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + + const others = []; + for (let playerID = 1; playerID < numPlayers; ++playerID) + { + if (playerID == player) + continue; + const cmpPlayer = QueryPlayerIDInterface(playerID); + if (cmpPlayer && cmpPlayer.GetState() == "defeated") + continue; + others.push(playerID); + } + + const mergedOwnerParams = Object.assign({}, parameters, ownerParameters); + const mergedOthersParams = Object.assign({}, parameters, othersParameters); + + // Auto-detect translatable parameters (those starting with "_" or named "player") + const translateParameters = Object.keys(mergedOthersParams).filter(key => key.startsWith("_") || key == "player"); + + TriggerHelper.SendNotification(ownerMessage, [player], duration, mergedOwnerParams, translateParameters); + TriggerHelper.SendNotification(othersMessage, others, duration, mergedOthersParams, translateParameters); +}; + +TriggerHelper.SendWonderNotifications = function(owner, othersMessage, ownerMessage) +{ + TriggerHelper.SendDualNotification(owner, ownerMessage, othersMessage, 30000, { "_player_": owner }); +}; + Engine.RegisterGlobal("TriggerHelper", TriggerHelper); diff --git a/binaries/data/mods/public/maps/scripts/WonderVictory.js b/binaries/data/mods/public/maps/scripts/WonderVictory.js index 7ca615412c..1942bdabfd 100644 --- a/binaries/data/mods/public/maps/scripts/WonderVictory.js +++ b/binaries/data/mods/public/maps/scripts/WonderVictory.js @@ -1,16 +1,42 @@ Trigger.prototype.WonderVictoryEntityRenamed = function(data) { - if (this.wonderVictoryMessages[data.entity] && Engine.QueryInterface(data.newentity, IID_Wonder)) - { - // When an entity is renamed, we first create a new entity, - // which in case it is a wonder will receive a timer. - // However on a rename we want to use the timer from the old entity, - // so we need to remove the timer of the new entity. - this.WonderVictoryDeleteTimer(data.newentity); + if (!Engine.QueryInterface(data.newentity, IID_Wonder)) + return; + // Check if wonder victory is enabled + const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); + if (!cmpEndGameManager) + return; + + const victoryConditions = cmpEndGameManager.GetVictoryConditions(); + if (!victoryConditions || !victoryConditions.includes("wonder")) + return; + + // Handle timer transfer (only if old entity had the timer) + if (this.wonderVictoryMessages[data.entity]) + { + this.WonderVictoryDeleteTimer(data.newentity); this.wonderVictoryMessages[data.newentity] = this.wonderVictoryMessages[data.entity]; delete this.wonderVictoryMessages[data.entity]; } + + // Make the completed wonder permanently visible + TriggerHelper.MakeEntityPermanentlyVisible(data.newentity, 50); + + // Send "wonder completed" notifications + const cmpOwnership = Engine.QueryInterface(data.newentity, IID_Ownership); + if (cmpOwnership) + { + const owner = cmpOwnership.GetOwner(); + if (owner > 0) + { + TriggerHelper.SendWonderNotifications( + owner, + markForTranslation("%(_player_)s has built a Wonder!"), + markForTranslation("You have built a Wonder!") + ); + } + } }; Trigger.prototype.WonderVictoryOwnershipChanged = function(data) @@ -18,6 +44,15 @@ Trigger.prototype.WonderVictoryOwnershipChanged = function(data) if (!Engine.QueryInterface(data.entity, IID_Wonder)) return; + // Check if wonder victory is enabled + const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); + if (!cmpEndGameManager) + return; + + const victoryConditions = cmpEndGameManager.GetVictoryConditions(); + if (!victoryConditions || !victoryConditions.includes("wonder")) + return; + this.WonderVictoryDeleteTimer(data.entity); if (data.to > 0) @@ -146,11 +181,46 @@ Trigger.prototype.WonderVictorySetWinner = function(playerID) n)); }; +Trigger.prototype.WonderStartNotification = function(data) +{ + // Check if wonder victory is enabled in game settings + const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); + if (!cmpEndGameManager) + return; + + // Don't show the wonder foundation if it's not a wonder victory game + const victoryConditions = cmpEndGameManager.GetVictoryConditions(); + if (!victoryConditions || !victoryConditions.includes("wonder")) + return; + + const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + if (!cmpTemplateManager) + return; + + const template = cmpTemplateManager.GetTemplate(data.template); + + if (!template || !("Wonder" in template)) + return; + + const owner = TriggerHelper.GetOwner(data.foundation); + if (owner <= 0) + return; + + TriggerHelper.MakeEntityPermanentlyVisible(data.foundation, 50); + + TriggerHelper.SendWonderNotifications( + owner, + markForTranslation("%(_player_)s has started building a Wonder."), + markForTranslation("You have started building a Wonder.") + ); +}; + { const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.RegisterTrigger("OnEntityRenamed", "WonderVictoryEntityRenamed", { "enabled": true }); cmpTrigger.RegisterTrigger("OnOwnershipChanged", "WonderVictoryOwnershipChanged", { "enabled": true }); cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "WonderVictoryDiplomacyChanged", { "enabled": true }); cmpTrigger.RegisterTrigger("OnPlayerWon", "WonderVictoryPlayerWon", { "enabled": true }); + cmpTrigger.RegisterTrigger("OnConstructionStarted", "WonderStartNotification", { "enabled": true }); cmpTrigger.wonderVictoryMessages = {}; } diff --git a/binaries/data/mods/public/simulation/components/Fogging.js b/binaries/data/mods/public/simulation/components/Fogging.js index a1905940f2..0c8a8c7cf5 100644 --- a/binaries/data/mods/public/simulation/components/Fogging.js +++ b/binaries/data/mods/public/simulation/components/Fogging.js @@ -29,6 +29,7 @@ Fogging.prototype.Init = function() this.mirages = []; this.miraged = []; this.seen = []; + this.revealed = []; const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let player = 0; player < numPlayers; ++player) @@ -36,6 +37,7 @@ Fogging.prototype.Init = function() this.mirages.push(INVALID_ENTITY); this.miraged.push(false); this.seen.push(false); + this.revealed.push(false); } }; @@ -169,6 +171,45 @@ Fogging.prototype.WasSeen = function(player) return this.seen[player]; }; +/** + * Permanently reveal this entity to one or all players. + * Destroys existing mirages and optionally deactivates fogging entirely. + * @param {number|string} player - Player ID to reveal to, or "all" for all players. + */ +Fogging.prototype.PermanentlyReveal = function(player = "all") +{ + const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + + // Validate single player case + if (player !== "all" && (player < 0 || player >= this.mirages.length)) + return; + + // Determine which players to reveal + const startPlayer = (player === "all") ? 0 : player; + const endPlayer = (player === "all") ? this.mirages.length : player + 1; + + // Reveal each player in the range + for (let i = startPlayer; i < endPlayer; ++i) + { + if (this.mirages[i] != INVALID_ENTITY) + { + Engine.DestroyEntity(this.mirages[i]); + this.mirages[i] = INVALID_ENTITY; + this.miraged[i] = false; + } + this.revealed[i] = true; + this.seen[i] = true; + } + + // Deactivate fogging if revealing to all players + if (player === "all") + this.activated = false; + + // Request visibility update + if (cmpRangeManager) + cmpRangeManager.RequestVisibilityUpdate(this.entity); +}; + Fogging.prototype.OnOwnershipChanged = function(msg) { // Always activate fogging for non-Gaia entities. @@ -209,7 +250,8 @@ Fogging.prototype.OnVisibilityChanged = function(msg) this.seen[msg.player] = true; } - if (msg.newVisibility == VIS_FOGGED && this.activated) + // Only create a mirage if the player hasn’t been permanently revealed + if (msg.newVisibility == VIS_FOGGED && this.activated && !this.revealed[msg.player]) this.LoadMirage(msg.player); };