Index: binaries/data/mods/public/gui/session/diplomacy_window.xml
===================================================================
--- binaries/data/mods/public/gui/session/diplomacy_window.xml	(révision 16529)
+++ binaries/data/mods/public/gui/session/diplomacy_window.xml	(copie de travail)
@@ -65,6 +65,10 @@
 				<object name="diplomacyPlayerTributeMetal[n]" size="100%-50 0 100%-30 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
 					<object name="diplomacyPlayerTributeMetalImage[n]" type="image" size="0 0 100% 100%" sprite="stretched:session/icons/resources/metal.png" ghost="true"/>
 				</object>
+
+				<object name="diplomacyAttackRequest[n]" size="100%-20 0 100% 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
+					<object name="diplomacyAttackRequestImage[n]" type="image" size="0 0 100% 100%" sprite="stretched:session/icons/stances/aggressive.png" ghost="true"/>
+				</object>
 			</object>
 		</repeat>
 	</object>
Index: binaries/data/mods/public/gui/session/menu.js
===================================================================
--- binaries/data/mods/public/gui/session/menu.js	(révision 16529)
+++ binaries/data/mods/public/gui/session/menu.js	(copie de travail)
@@ -301,6 +301,7 @@
 			// Hide the unused/unselectable options
 			for each (let a in ["TributeFood", "TributeWood", "TributeStone", "TributeMetal", "Ally", "Neutral", "Enemy"])
 				Engine.GetGUIObjectByName("diplomacyPlayer"+a+"["+(i-1)+"]").hidden = true;
+			Engine.GetGUIObjectByName("diplomacyAttackRequest["+(i-1)+"]").hidden = true;
 			continue;
 		}
 
@@ -341,6 +342,14 @@
 			button.tooltip = formatTributeTooltip(g_Players[i], resource, 100);
 		}
 
+		// Attack Request
+		let button = Engine.GetGUIObjectByName("diplomacyAttackRequest["+(i-1)+"]");
+		button.hidden = !(g_Players[i].isEnemy[we]);
+		button.tooltip = "request for your allies to attack this enemy";
+		button.onpress = (function(i, we){ return function() {
+			Engine.PostNetworkCommand({"type": "attack-request", "source": we, "target": i});
+		} })(i, we);
+
 		// Skip our own teams on teams locked
 		if (g_Players[we].teamsLocked && g_Players[we].team != -1 && g_Players[we].team == g_Players[i].team)
 			continue;
Index: binaries/data/mods/public/gui/session/messages.js
===================================================================
--- binaries/data/mods/public/gui/session/messages.js	(révision 16529)
+++ binaries/data/mods/public/gui/session/messages.js	(copie de travail)
@@ -57,6 +57,14 @@
 		{
 			message["translateParameters"] = notification["translateParameters"];
 			message["parameters"] = notification["parameters"];
+			// special case for formatting of player names which are transmitted as _player_num
+			for (let param in message["parameters"])
+			{
+				if (!message["parameters"][param].startsWith("_player_"))
+					continue;
+				let colorName = getUsernameAndColor(+message["parameters"][param].substr(8));
+				message["parameters"][param] = "[color=\"" + colorName[1] + "\"]" + colorName[0] + "[/color]";
+			}
 		}
 		var guid = findGuidForPlayerID(g_PlayerAssignments, player);
 		if (guid == undefined)
Index: binaries/data/mods/public/simulation/ai/petra/attackManager.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/attackManager.js	(révision 16529)
+++ binaries/data/mods/public/simulation/ai/petra/attackManager.js	(copie de travail)
@@ -46,6 +46,69 @@
 	}
 };
 
+m.AttackManager.prototype.checkEvents = function(gameState, events)
+{
+	let PlayerEvents = events["AttackRequest"];
+	let answer = false;
+	let other = undefined;
+	let targetPlayer = undefined;
+	for (let evt of PlayerEvents)
+	{
+		if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.target))
+			continue;
+		targetPlayer = evt.target;
+		let available = 0;
+		for (let attackType in this.upcomingAttacks)
+		{
+			for (let attack of this.upcomingAttacks[attackType])
+			{
+				if (attack.state === "completing")
+				{
+					if (attack.targetPlayer && attack.targetPlayer === targetPlayer)
+						available += attack.unitCollection.length;
+					else if (attack.targetPlayer && attack.targetPlayer !== targetPlayer)
+						other = attack.targetPlayer;
+					continue;
+				}
+				if (!attack.targetPlayer || attack.targetPlayer !== targetPlayer)
+				{
+					let oldTargetPlayer = attack.targetPlayer;
+					let oldTarget = attack.target;
+					attack.targetPlayer = targetPlayer;
+					attack.target = attack.getNearestTarget(gameState, attack.rallyPoint);
+					if (!attack.target)
+					{
+						attack.targetPlayer = oldTargetPlayer;
+						attack.target = oldTarget;
+						continue;
+					}
+				}
+				if (attack.targetPlayer && attack.targetPlayer === targetPlayer)
+					available += attack.unitCollection.length;
+			}
+		}
+
+		if (available > 12)	// launch the attack immediately
+		{
+			for (let attackType in this.upcomingAttacks)
+			{
+				for (let attack of this.upcomingAttacks[attackType])
+				{
+					if (attack.state !== "completing" && attack.targetPlayer && attack.targetPlayer === targetPlayer)
+					{
+						attack.forceStart();
+						attack.requested = true;
+					}
+				}
+			}
+			answer = true;
+		}
+		break;  // take only the first attack request into account
+	}
+	if (targetPlayer)
+		m.chatAnswerRequestAttack(gameState, targetPlayer, answer, other);
+};
+
 // Some functions are run every turn
 // Others once in a while
 m.AttackManager.prototype.update = function(gameState, queues, events)
@@ -64,21 +127,26 @@
 		API3.warn(" ==================================");
 	}
 
-	for (var attackType in this.upcomingAttacks)
+	this.checkEvents(gameState, events);
+
+	let unexecutedAttacks = {"Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0};
+	for (let attackType in this.upcomingAttacks)
 	{
-		for (var i = 0; i < this.upcomingAttacks[attackType].length; ++i)
+		for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i)
 		{
-			var attack = this.upcomingAttacks[attackType][i];
+			let attack = this.upcomingAttacks[attackType][i];
 			attack.checkEvents(gameState, events);
 
 			if (attack.isStarted())
 				API3.warn("Petra problem in attackManager: attack in preparation has already started ???");
 
-			var updateStep = attack.updatePreparation(gameState);
+			let updateStep = attack.updatePreparation(gameState);
 			// now we're gonna check if the preparation time is over
 			if (updateStep === 1 || attack.isPaused() )
 			{
 				// just chillin'
+				if (attack.state === "unexecuted")
+					++unexecutedAttacks[attackType];
 			}
 			else if (updateStep === 0 || updateStep === 3)
 			{
@@ -96,7 +164,6 @@
 					if (this.Config.chat)
 						m.chatLaunchAttack(gameState, attack.targetPlayer);
 					this.startedAttacks[attackType].push(attack);
-//					Engine.PostCommand(PlayerID, {"type": "aievent", "from": PlayerID, "action": "attack", "target": attack.targetPlayer});
 				}
 				else
 					attack.Abort(gameState);
@@ -130,7 +197,7 @@
 	var barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length;
 	if (this.rushNumber < this.maxRushes && barracksNb >= 1)
 	{
-		if (this.upcomingAttacks["Rush"].length === 0)
+		if (unexecutedAttacks["Rush"] === 0)
 		{
 			// we have a barracks and we want to rush, rush.
 			var data = { "targetSize": this.rushSize[this.rushNumber] };
@@ -146,7 +213,7 @@
 			this.rushNumber++;
 		}
 	}
-	else if (this.upcomingAttacks["Attack"].length === 0 && this.upcomingAttacks["HugeAttack"].length === 0
+	else if (unexecutedAttacks["Attack"] === 0 && unexecutedAttacks["HugeAttack"] === 0
 		&& (this.startedAttacks["Attack"].length + this.startedAttacks["HugeAttack"].length < Math.round(gameState.getPopulationMax()/100)))
 	{
 		if ((barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase())))
@@ -172,7 +239,7 @@
 		}
 	}
 
-	if (this.upcomingAttacks["Raid"].length === 0 && gameState.ai.HQ.defenseManager.targetList.length)
+	if (unexecutedAttacks["Raid"] === 0 && gameState.ai.HQ.defenseManager.targetList.length)
 	{
 		var target = undefined;
 		for (var targetId of gameState.ai.HQ.defenseManager.targetList)
Index: binaries/data/mods/public/simulation/ai/petra/attackPlan.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/attackPlan.js	(révision 16529)
+++ binaries/data/mods/public/simulation/ai/petra/attackPlan.js	(copie de travail)
@@ -266,6 +266,16 @@
 	return false;
 };
 
+m.AttackPlan.prototype.forceStart = function()
+{
+	for (let unitCat in this.unitStat)
+	{
+		let Unit = this.unitStat[unitCat];
+		Unit["targetSize"] = 0;
+		Unit["minSize"] = 0;
+	}
+};
+
 // Adds a build order. If resetQueue is true, this will reset the queue.
 m.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue)
 {
@@ -465,7 +475,12 @@
 	if (this.type === "Raid")
 		this.maxCompletingTurn = gameState.ai.playedTurn + 20;
 	else
+	{
 		this.maxCompletingTurn = gameState.ai.playedTurn + 60;
+		// warn our allies so that they can help if possible
+		if (!this.requested)
+			Engine.PostCommand(PlayerID, {"type": "attack-request", "source": PlayerID, "target": this.targetPlayer});
+	}
 
 	var rallyPoint = this.rallyPoint;
 	var rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint);
Index: binaries/data/mods/public/simulation/ai/petra/chatHelper.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/chatHelper.js	(révision 16529)
+++ binaries/data/mods/public/simulation/ai/petra/chatHelper.js	(copie de travail)
@@ -3,7 +3,6 @@
 
 m.chatLaunchAttack = function(gameState, player)
 {
-	var name = gameState.sharedScript.playersData[player].name;
 	var proba = Math.random();
 	if (proba < 0.5)
 		var message = "/team " + markForTranslation("I am launching an attack against %(name)s.");
@@ -10,13 +9,51 @@
 	else
 		var message = "/team " + markForTranslation("I have just sent an army against %(name)s.");
 
-	var chat = { "type": "aichat", "message": message, "translateMessage": true, "translateParameters": ["name"], "parameters": { "name": name } };
+	var chat = {
+		"type": "aichat",
+		"message": message,
+		"translateMessage": true,
+		"translateParameters": ["name"],
+		"parameters": {"name": "_player_" + player}
+	};
 	Engine.PostCommand(PlayerID, chat);
 };
 
+m.chatAnswerRequestAttack = function(gameState, player, answer, other)
+{
+	if (answer)
+	{
+		var proba = Math.random();
+		if (proba < 0.5)
+			var message = "/team " + markForTranslation("Let me regroup my army and I am with you against %(name)s.");
+		else
+			var message = "/team " + markForTranslation("I am doing the final preparation and I will attack %(name)s.");
+	}
+	else
+	{
+		if (other !== undefined)
+			var message = "/team " + markForTranslation("I cannot help you against %(name)s for the time being, as I have another attack foreseen against %(other)s.");
+		else
+			var message = "/team " + markForTranslation("Sorry, I have not enough soldiers currently, but my next attack will target %(name)s.");
+	}
+
+	var chat = {
+		"type": "aichat",
+		"message": message,
+		"translateMessage": true,
+		"translateParameters": ["name"],
+		"parameters": {"name": "_player_" + player}
+	};
+	if (other !== undefined)
+	{
+		chat.translateParameters.push("other");
+		chat.parameters.other = "_player_" + other;
+	}
+	Engine.PostCommand(PlayerID, chat);
+};
+
 m.chatSentTribute = function(gameState, player)
 {
-	var name = gameState.sharedScript.playersData[player].name;
 	var proba = Math.random();
 	if (proba < 0.5)
 		var message = "/team " + markForTranslation("Here is a gift for %(name)s, make a good use of it.");
@@ -23,7 +60,13 @@
 	else
 		var message = "/team " + markForTranslation("I see you are in a bad situation %(name)s, I hope this will help."); 
 
-	var chat = { "type": "aichat", "message": message, "translateMessage": true, "translateParameters": ["name"], "parameters": { "name": name } };
+	var chat = {
+		"type": "aichat",
+		"message": message,
+		"translateMessage": true,
+		"translateParameters": ["name"],
+		"parameters": {"name": "_player_" + player}
+	};
 	Engine.PostCommand(PlayerID, chat);
 };
 
@@ -35,13 +78,18 @@
 	else
 		var message = "/team " + markForTranslation("I would participate more efficiently in our common war effort if you could provide me some %(resource)s.");
 
-	var chat = { "type": "aichat", "message": message, "translateMessage": true, "translateParameters": ["resource"], "parameters": { "resource": resource } };
+	var chat = {
+		"type": "aichat",
+		"message": message,
+		"translateMessage": true,
+		"translateParameters": ["resource"],
+		"parameters": {"resource": resource}
+	};
 	Engine.PostCommand(PlayerID, chat);
 };
 
 m.chatNewTradeRoute = function(gameState, player)
 {
-	var name = gameState.sharedScript.playersData[player].name;
 	var proba = Math.random();
 	if (proba < 0.5)
 		var message = "/team " + markForTranslation("I have set up a new route with %(name)s. Trading will be profitable for all of us.");
@@ -48,7 +96,13 @@
 	else
 		var message = "/team " + markForTranslation("A new trade route is set up with %(name)s. Take your share of the profits");
 
-	var chat = { "type": "aichat", "message": message, "translateMessage": true, "translateParameters": ["name"], "parameters": { "name": name } };
+	var chat = {
+		"type": "aichat",
+		"message": message,
+		"translateMessage": true,
+		"translateParameters": ["name"],
+		"parameters": {"name": "_player_" + player}
+	};
 	Engine.PostCommand(PlayerID, chat);
 };
 
Index: binaries/data/mods/public/simulation/components/AIInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/AIInterface.js	(révision 16529)
+++ binaries/data/mods/public/simulation/components/AIInterface.js	(copie de travail)
@@ -17,7 +17,8 @@
 	"OwnershipChanged",
 	"Garrison",
 	"UnGarrison",
-	"TributeExchanged"
+	"TributeExchanged",
+	"AttackRequest"
 ];
 
 AIInterface.prototype.Init = function()
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js	(révision 16529)
+++ binaries/data/mods/public/simulation/helpers/Commands.js	(copie de travail)
@@ -644,6 +644,28 @@
 			}
 		}
 	},
+
+	"attack-request": function(player, cmd, data)
+	{
+		// Send a chat message to human players
+		var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+		if (cmpGuiInterface)
+		{
+			var notification = {
+				"type": "aichat",
+				"players": [player],
+				"message": "/team " + markForTranslation("Attack against %(target)s requested."),
+				"translateParameters": ["target"],
+				"parameters": {"target": "_player_" + cmd.target}
+			};
+			cmpGuiInterface.PushNotification(notification);
+		}
+		// And send an attackRequest event to the AIs
+		let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
+		if (cmpAIInterface)
+			cmpAIInterface.PushEvent("AttackRequest", cmd);
+	},
+
 	"dialog-answer": function(player, cmd, data)
 	{
 		// Currently nothing. Triggers can read it anyway, and send this
