Index: ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json +++ ps/trunk/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"}, Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js @@ -126,8 +126,7 @@ ), "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 @@ }, "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 @@ 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 }); } Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js +++ ps/trunk/binaries/data/mods/public/gui/session/session.js @@ -546,23 +546,35 @@ } /** - * Called when a player has won or was defeated. - */ -function playerFinished(player, won) -{ - if (player == Engine.GetPlayerID()) + * 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(); - 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 @@ return; Engine.PostNetworkCommand({ - "type": "defeat-player", - "playerId": Engine.GetPlayerID(), - "resign": true + "type": "resign" }); if (!leaveGameAfterResign) Index: ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js +++ ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js @@ -345,7 +345,10 @@ 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]) { Index: ps/trunk/binaries/data/mods/public/maps/scenarios/treasure_islands.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scenarios/treasure_islands.js +++ ps/trunk/binaries/data/mods/public/maps/scenarios/treasure_islands.js @@ -96,7 +96,16 @@ 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); Index: ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js +++ ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js @@ -145,8 +145,23 @@ "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)); }; { Index: ps/trunk/binaries/data/mods/public/maps/scripts/Conquest.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/Conquest.js +++ ps/trunk/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); Index: ps/trunk/binaries/data/mods/public/maps/scripts/ConquestCommon.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/ConquestCommon.js +++ ps/trunk/binaries/data/mods/public/maps/scripts/ConquestCommon.js @@ -29,7 +29,7 @@ { let cmpPlayer = QueryPlayerIDInterface(msg.from); if (cmpPlayer) - cmpPlayer.SetState("defeated"); + cmpPlayer.SetState("defeated", this.conquestDefeatReason); } } }; Index: ps/trunk/binaries/data/mods/public/maps/scripts/ConquestStructures.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/ConquestStructures.js +++ ps/trunk/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); Index: ps/trunk/binaries/data/mods/public/maps/scripts/ConquestUnits.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/ConquestUnits.js +++ ps/trunk/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); Index: ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js +++ ps/trunk/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) Index: ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js +++ ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js @@ -116,22 +116,34 @@ /** * 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); }; /** Index: ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js +++ ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js @@ -56,8 +56,8 @@ "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.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 }); Index: ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js +++ ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js @@ -48,13 +48,28 @@ 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 @@ 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 @@ } 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."), Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js @@ -395,7 +395,13 @@ 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 @@ 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() Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js @@ -50,7 +50,7 @@ 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); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js @@ -437,11 +437,11 @@ } }, - "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)