diff --git a/binaries/data/mods/public/gui/credits/texts/programming.json b/binaries/data/mods/public/gui/credits/texts/programming.json index bc51bb4b77..26f778fc43 100644 --- a/binaries/data/mods/public/gui/credits/texts/programming.json +++ b/binaries/data/mods/public/gui/credits/texts/programming.json @@ -25,6 +25,7 @@ {"nick": "aBothe", "name": "Alexander Bothe"}, {"nick": "alpha123", "name": "Peter P. Cannici"}, {"nick": "andy5995", "name": "Andy Alt"}, + {"nick": "Angen"}, {"nick": "ArnH", "name": "Arno Hemelhof"}, {"nick": "Aurium", "name": "Aurélio Heckert"}, {"nick": "badmadblacksad", "name": "Martin F"}, diff --git a/binaries/data/mods/public/gui/session/messages.js b/binaries/data/mods/public/gui/session/messages.js index 963fc96b1f..52c1cdc924 100644 --- a/binaries/data/mods/public/gui/session/messages.js +++ b/binaries/data/mods/public/gui/session/messages.js @@ -126,8 +126,7 @@ var g_FormatChatMessage = { ), "clientlist": msg => getUsernameList(), "message": msg => formatChatCommand(msg), - "defeat": msg => formatDefeatMessage(msg), - "won": msg => formatWinMessage(msg), + "defeat-victory": msg => formatDefeatVictoryMessage(msg.message, msg.players), "diplomacy": msg => formatDiplomacyMessage(msg), "tribute": msg => formatTributeMessage(msg), "barter": msg => formatBarterMessage(msg), @@ -315,24 +314,11 @@ var g_NotificationsTypes = }, "defeat": function(notification, player) { - addChatMessage({ - "type": "defeat", - "guid": findGuidForPlayerID(player), - "player": player, - "resign": !!notification.resign - }); - playerFinished(player, false); - sendLobbyPlayerlistUpdate(); + playersFinished(notification.allies, notification.message, false); }, "won": function(notification, player) { - addChatMessage({ - "type": "won", - "guid": findGuidForPlayerID(player), - "player": player - }); - playerFinished(player, true); - sendLobbyPlayerlistUpdate(); + playersFinished(notification.allies, notification.message, true); }, "diplomacy": function(notification, player) { @@ -966,20 +952,20 @@ function colorizePlayernameParameters(parameters) parameters[param] = colorizePlayernameByID(parameters[param]); } -function formatDefeatMessage(msg) +function formatDefeatVictoryMessage(message, players) { - return sprintf( - msg.resign ? - translate("%(player)s has resigned.") : - translate("%(player)s has been defeated."), - { "player": colorizePlayernameByID(msg.player) } - ); -} + if (!message.pluralMessage) + return sprintf(translate(message), { + "player": colorizePlayernameByID(players[0]) + }); -function formatWinMessage(msg) -{ - return sprintf(translate("%(player)s has won."), { - "player": colorizePlayernameByID(msg.player) + let mPlayers = players.map(playerID => colorizePlayernameByID(playerID)); + let lastPlayer = mPlayers.pop(); + + return sprintf(translatePlural(message.message, message.pluralMessage, message.pluralCount), { + // Translation: This comma is used for separating first to penultimate elements in an enumeration. + "players": mPlayers.join(translate(", ")), + "lastPlayer": lastPlayer }); } diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js index 1baa87dabf..a27e2cbbdf 100644 --- a/binaries/data/mods/public/gui/session/session.js +++ b/binaries/data/mods/public/gui/session/session.js @@ -546,23 +546,35 @@ function controlsPlayer(playerID) } /** - * Called when a player has won or was 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 playerFinished(player, won) +function playersFinished(players, victoryString, won) { - if (player == Engine.GetPlayerID()) + addChatMessage({ + "type": "defeat-victory", + "message": victoryString, + "players": players + }); + + if (players.indexOf(Engine.GetPlayerID()) != -1) reportGame(); + sendLobbyPlayerlistUpdate(); + updatePlayerData(); updateChatAddressees(); - if (player != g_ViewedPlayer) + 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 (player != Engine.GetPlayerID() || Engine.IsAtlasRunning()) + if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning()) return; global.music.setState( @@ -657,9 +669,7 @@ function resignGame(leaveGameAfterResign) return; Engine.PostNetworkCommand({ - "type": "defeat-player", - "playerId": Engine.GetPlayerID(), - "resign": true + "type": "resign" }); if (!leaveGameAfterResign) diff --git a/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js b/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js index fa44b3ea6b..11530fd9e6 100644 --- a/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js +++ b/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js @@ -345,7 +345,10 @@ Trigger.prototype.OnOwnershipChanged = function(data) if (data.entity == this.playerCivicCenter[data.from]) { this.playerCivicCenter[data.from] = undefined; - TriggerHelper.DefeatPlayer(data.from); + + TriggerHelper.DefeatPlayer( + data.from, + markForTranslation("%(player)s has been defeated (lost civic center).")); } else if (data.entity == this.treasureFemale[data.from]) { diff --git a/binaries/data/mods/public/maps/scenarios/treasure_islands.js b/binaries/data/mods/public/maps/scenarios/treasure_islands.js index a9eb1f324e..d31c3b67a3 100644 --- a/binaries/data/mods/public/maps/scenarios/treasure_islands.js +++ b/binaries/data/mods/public/maps/scenarios/treasure_islands.js @@ -96,7 +96,16 @@ Trigger.prototype.BattleMessage = function() Trigger.prototype.Victory = function(playerID) { - TriggerHelper.SetPlayerWon(playerID); + TriggerHelper.SetPlayerWon( + playerID, + n => markForPluralTranslation( + "%(lastPlayer)s has won (treasure collected).", + "%(players)s and %(lastPlayer)s have won (treasure collected).", + n), + n => markForPluralTranslation( + "%(lastPlayer)s has been defeated (treasure collected).", + "%(players)s and %(lastPlayer)s have been defeated (treasure collected).", + n)); }; var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); diff --git a/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js b/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js index 492c5f09a0..1d0cebb1d1 100644 --- a/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js +++ b/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js @@ -145,8 +145,23 @@ Trigger.prototype.StartCaptureTheRelicCountdown = function(playerAndAllies) "translateMessage": true }, captureTheRelicDuration); - this.relicsVictoryTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_EndGameManager, - "MarkPlayerAsWon", captureTheRelicDuration, playerAndAllies[0]); + this.relicsVictoryTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, + "CaptureTheRelicVictorySetWinner", captureTheRelicDuration, playerAndAllies[0]); +}; + +Trigger.prototype.CaptureTheRelicVictorySetWinner = function(playerID) +{ + let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); + cmpEndGameManager.MarkPlayerAsWon( + playerID, + n => markForPluralTranslation( + "%(lastPlayer)s has won (Capture the Relic).", + "%(players)s and %(lastPlayer)s have won (Capture the Relic).", + n), + n => markForPluralTranslation( + "%(lastPlayer)s has been defeated (Capture the Relic).", + "%(players)s and %(lastPlayer)s have been defeated (Capture the Relic).", + n)); }; { diff --git a/binaries/data/mods/public/maps/scripts/Conquest.js b/binaries/data/mods/public/maps/scripts/Conquest.js index a8f50f9416..4e9533c8f7 100644 --- a/binaries/data/mods/public/maps/scripts/Conquest.js +++ b/binaries/data/mods/public/maps/scripts/Conquest.js @@ -1,6 +1,7 @@ { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.conquestClassFilter = "ConquestCritical"; + cmpTrigger.conquestDefeatReason = markForTranslation("%(player)s has been defeated (lost all workers and structures)."); let data = { "enabled": true }; cmpTrigger.RegisterTrigger("OnOwnershipChanged", "ConquestHandlerOwnerShipChanged", data); diff --git a/binaries/data/mods/public/maps/scripts/ConquestCommon.js b/binaries/data/mods/public/maps/scripts/ConquestCommon.js index 44b03bc473..cabb1d5b99 100644 --- a/binaries/data/mods/public/maps/scripts/ConquestCommon.js +++ b/binaries/data/mods/public/maps/scripts/ConquestCommon.js @@ -29,7 +29,7 @@ Trigger.prototype.ConquestHandlerOwnerShipChanged = function(msg) { let cmpPlayer = QueryPlayerIDInterface(msg.from); if (cmpPlayer) - cmpPlayer.SetState("defeated"); + cmpPlayer.SetState("defeated", this.conquestDefeatReason); } } }; diff --git a/binaries/data/mods/public/maps/scripts/ConquestStructures.js b/binaries/data/mods/public/maps/scripts/ConquestStructures.js index a84fe95351..a062fbedb2 100644 --- a/binaries/data/mods/public/maps/scripts/ConquestStructures.js +++ b/binaries/data/mods/public/maps/scripts/ConquestStructures.js @@ -1,6 +1,7 @@ { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.conquestClassFilter = "Structure"; + cmpTrigger.conquestDefeatReason = markForTranslation("%(player)s has been defeated (lost all structures)."); let data = { "enabled": true }; cmpTrigger.RegisterTrigger("OnOwnershipChanged", "ConquestHandlerOwnerShipChanged", data); diff --git a/binaries/data/mods/public/maps/scripts/ConquestUnits.js b/binaries/data/mods/public/maps/scripts/ConquestUnits.js index 53cf6f26f8..9ba0331ec8 100644 --- a/binaries/data/mods/public/maps/scripts/ConquestUnits.js +++ b/binaries/data/mods/public/maps/scripts/ConquestUnits.js @@ -1,6 +1,7 @@ { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.conquestClassFilter = "Unit"; + cmpTrigger.conquestDefeatReason = markForTranslation("%(player)s has been defeated (lost all workers)."); let data = { "enabled": true }; cmpTrigger.RegisterTrigger("OnOwnershipChanged", "ConquestHandlerOwnerShipChanged", data); diff --git a/binaries/data/mods/public/maps/scripts/Regicide.js b/binaries/data/mods/public/maps/scripts/Regicide.js index 5596ea6564..7b70915afe 100644 --- a/binaries/data/mods/public/maps/scripts/Regicide.js +++ b/binaries/data/mods/public/maps/scripts/Regicide.js @@ -1,7 +1,9 @@ Trigger.prototype.CheckRegicideDefeat = function(data) { if (data.entity == this.regicideHeroes[data.from]) - TriggerHelper.DefeatPlayer(data.from); + TriggerHelper.DefeatPlayer( + data.from, + markForTranslation("%(player)s has been defeated (lost hero).")); }; Trigger.prototype.InitRegicideGame = function(msg) diff --git a/binaries/data/mods/public/maps/scripts/TriggerHelper.js b/binaries/data/mods/public/maps/scripts/TriggerHelper.js index e855bc8eef..21802bc5ed 100644 --- a/binaries/data/mods/public/maps/scripts/TriggerHelper.js +++ b/binaries/data/mods/public/maps/scripts/TriggerHelper.js @@ -116,22 +116,34 @@ TriggerHelper.GetResourceType = function(entity) /** * The given player will win the game. - * If it's not a last man standing game, then allies will win too. + * If it's not a last man standing game, then allies will win too and others will be defeated. + * + * @param {number} playerID - The player who will win. + * @param {function} victoryReason - Function that maps from number to plural string, for example + * n => markForPluralTranslation( + * "%(lastPlayer)s has won (game mode).", + * "%(players)s and %(lastPlayer)s have won (game mode).", + * n)); + * It's a function since we don't know in advance how many players will have won. */ -TriggerHelper.SetPlayerWon = function(playerID) +TriggerHelper.SetPlayerWon = function(playerID, victoryReason, defeatReason) { let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); - cmpEndGameManager.MarkPlayerAsWon(playerID); + cmpEndGameManager.MarkPlayerAsWon(playerID, victoryReason, defeatReason); }; /** - * Defeats a player + * Defeats a single player. + * + * @param {number} - ID of that player. + * @param {string} - String to be shown in chat, for example + * markForTranslation("%(player)s has been defeated (objective).") */ -TriggerHelper.DefeatPlayer = function(playerID) +TriggerHelper.DefeatPlayer = function(playerID, defeatReason) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer) - cmpPlayer.SetState("defeated"); + cmpPlayer.SetState("defeated", defeatReason); }; /** diff --git a/binaries/data/mods/public/maps/scripts/WonderVictory.js b/binaries/data/mods/public/maps/scripts/WonderVictory.js index 5dbb873bd0..5d1d978df2 100644 --- a/binaries/data/mods/public/maps/scripts/WonderVictory.js +++ b/binaries/data/mods/public/maps/scripts/WonderVictory.js @@ -56,8 +56,8 @@ Trigger.prototype.CheckWonderVictory = function(data) "translateMessage": true, }, wonderDuration); - timer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_EndGameManager, - "MarkPlayerAsWon", wonderDuration, data.to); + timer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, + "WonderVictorySetWinner", wonderDuration, data.to); this.wonderVictoryTimers[ent] = timer; this.wonderVictoryMessages[ent] = messages; @@ -76,6 +76,21 @@ Trigger.prototype.DeleteWonderVictoryMessages = function(data) } }; +Trigger.prototype.WonderVictorySetWinner = function(playerID) +{ + let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); + cmpEndGameManager.MarkPlayerAsWon( + playerID, + n => markForPluralTranslation( + "%(lastPlayer)s has won (wonder victory).", + "%(players)s and %(lastPlayer)s have won (wonder victory).", + n), + n => markForPluralTranslation( + "%(lastPlayer)s has been defeated (wonder victory).", + "%(players)s and %(lastPlayer)s have been defeated (wonder victory).", + n)); +}; + { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckWonderVictory", { "enabled": true }); diff --git a/binaries/data/mods/public/simulation/components/EndGameManager.js b/binaries/data/mods/public/simulation/components/EndGameManager.js index 8ba7821676..9e88a42cf3 100644 --- a/binaries/data/mods/public/simulation/components/EndGameManager.js +++ b/binaries/data/mods/public/simulation/components/EndGameManager.js @@ -48,13 +48,28 @@ EndGameManager.prototype.SetGameType = function(newGameType, newSettings = {}) Engine.BroadcastMessage(MT_GameTypeChanged, {}); }; -EndGameManager.prototype.MarkPlayerAsWon = function(playerID) +/** + * Sets the given player (and the allies if allied victory is enabled) as a winner. + * + * @param {number} playerID - The player that should win. + * @param {function} victoryReason - Function that maps from number to plural string, for example + * n => markForPluralTranslation( + * "%(lastPlayer)s has won (game mode).", + * "%(players)s and %(lastPlayer)s have won (game mode).", + * n)); + */ +EndGameManager.prototype.MarkPlayerAsWon = function(playerID, victoryString, defeatString) { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let numPlayers = cmpPlayerManager.GetNumPlayers(); this.skipAlliedVictoryCheck = true; + let winningPlayers = []; + let defeatedPlayers = []; + + let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + // Group win/defeat messages for (let won of [false, true]) for (let i = 1; i < numPlayers; ++i) @@ -63,9 +78,36 @@ EndGameManager.prototype.MarkPlayerAsWon = function(playerID) let hasWon = playerID == i || this.alliedVictory && cmpPlayer.IsMutualAlly(playerID); if (hasWon == won) - cmpPlayer.SetState(won ? "won" : "defeated"); + { + if (won) + { + cmpPlayer.SetState("won", undefined); + winningPlayers.push(i); + } + else + { + cmpPlayer.SetState("defeated", undefined); + defeatedPlayers.push(i); + } + } } + if (winningPlayers.length) + cmpGUIInterface.PushNotification({ + "type": "won", + "players": [winningPlayers[0]], + "allies" : winningPlayers, + "message": victoryString(winningPlayers.length) + }); + + if (defeatedPlayers.length) + cmpGUIInterface.PushNotification({ + "type": "defeat", + "players": [defeatedPlayers[0]], + "allies" : defeatedPlayers, + "message": defeatString(defeatedPlayers.length) + }); + this.skipAlliedVictoryCheck = false; }; @@ -106,12 +148,24 @@ EndGameManager.prototype.AlliedVictoryCheck = function() } if (this.alliedVictory || allies.length == 1) + { for (let playerID of allies) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer) - cmpPlayer.SetState("won"); + cmpPlayer.SetState("won", undefined); } + + cmpGuiInterface.PushNotification({ + "type": "won", + "players": [allies[0]], + "allies" : allies, + "message": markForPluralTranslation( + "%(lastPlayer)s has won (last player alive).", + "%(players)s and %(lastPlayer)s have won (last players alive).", + allies.length) + }); + } else this.lastManStandingMessage = cmpGuiInterface.AddTimeNotification({ "message": markForTranslation("Last remaining player wins."), diff --git a/binaries/data/mods/public/simulation/components/Player.js b/binaries/data/mods/public/simulation/components/Player.js index e9dea923a2..728bffd742 100644 --- a/binaries/data/mods/public/simulation/components/Player.js +++ b/binaries/data/mods/public/simulation/components/Player.js @@ -395,7 +395,13 @@ Player.prototype.GetState = function() return this.state; }; -Player.prototype.SetState = function(newState, resign) +/** + * @param {string} newState - Either "defeated" or "won". + * @param {string|undefined} message - A string to be shown in chat, for example + * markForTranslation("%(player)s has been defeated (failed objective)."). + * If it is undefined, the caller MUST send that GUI notification manually. + */ +Player.prototype.SetState = function(newState, message) { if (this.state != "active") return; @@ -438,17 +444,21 @@ Player.prototype.SetState = function(newState, resign) Engine.BroadcastMessage(won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID }); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - if (won) - cmpGUIInterface.PushNotification({ - "type": "won", - "players": [this.playerID] - }); - else - cmpGUIInterface.PushNotification({ - "type": "defeat", - "players": [this.playerID], - "resign": resign - }); + if (message) + if (won) + cmpGUIInterface.PushNotification({ + "type": "won", + "players": [this.playerID], + "allies": [this.playerID], + "message": message + }); + else + cmpGUIInterface.PushNotification({ + "type": "defeat", + "players": [this.playerID], + "allies": [this.playerID], + "message": message + }); }; Player.prototype.GetTeam = function() diff --git a/binaries/data/mods/public/simulation/helpers/Cheat.js b/binaries/data/mods/public/simulation/helpers/Cheat.js index 3dae379d88..a8c5599f78 100644 --- a/binaries/data/mods/public/simulation/helpers/Cheat.js +++ b/binaries/data/mods/public/simulation/helpers/Cheat.js @@ -50,7 +50,7 @@ function Cheat(input) case "defeatplayer": cmpPlayer = QueryPlayerIDInterface(input.parameter); if (cmpPlayer) - cmpPlayer.SetState("defeated"); + cmpPlayer.SetState("defeated", markForTranslation("%(player)s has been defeated (cheat).")); return; case "createunits": var cmpProductionQueue = input.selected.length && Engine.QueryInterface(input.selected[0], IID_ProductionQueue); diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index 8f9789f87a..03a332c599 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -437,11 +437,11 @@ var g_Commands = { } }, - "defeat-player": function(player, cmd, data) + "resign": function(player, cmd, data) { let cmpPlayer = QueryPlayerIDInterface(player); if (cmpPlayer) - cmpPlayer.SetState("defeated", !!cmd.resign); + cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned.")); }, "garrison": function(player, cmd, data)