Index: binaries/data/mods/public/gui/session/diplomacy_window.xml
===================================================================
--- binaries/data/mods/public/gui/session/diplomacy_window.xml	(révision 16505)
+++ 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 16505)
+++ binaries/data/mods/public/gui/session/menu.js	(copie de travail)
@@ -261,6 +261,11 @@
 	Engine.PostNetworkCommand({"type": "tribute", "player": data.player, "amounts":  data.amounts});
 }
 
+function requestAttack(data)
+{
+	Engine.PostNetworkCommand({"type": "aievent", "from": data.from, "action": data.action, "target":  data.target});
+}
+
 function openDiplomacy()
 {
 	if (isTradeOpen)
@@ -301,6 +306,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 +347,15 @@
 			button.tooltip = formatTributeTooltip(g_Players[i], resource, 100);
 		}
 
+		if (!g_IsNetworked)  // TODO in multiplayer, should be done if some AIs
+		{
+			// Attack Request (used to communicate with AI)
+			let button = Engine.GetGUIObjectByName("diplomacyAttackRequest["+(i-1)+"]");
+			button.hidden = !(g_Players[i].isEnemy[we]);
+			button.tooltip = "request for your AI allies to attack this enemy";
+			button.onpress = (function(i, we){ return function() { requestAttack({"type": "aievent", "from": we, "action": "attack", "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/simulation/ai/petra/attackManager.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/attackManager.js	(révision 16505)
+++ binaries/data/mods/public/simulation/ai/petra/attackManager.js	(copie de travail)
@@ -46,6 +46,64 @@
 	}
 };
 
+m.AttackManager.prototype.checkEvents = function(gameState, events)
+{
+	let PlayerEvents = events["PlayerMessage"];
+	let answer = false;
+	let other = false;
+	let targetPlayer = undefined;
+	for (let evt of PlayerEvents)
+	{
+		if (evt.action !== "attack")
+			continue;
+		if (evt.from === PlayerID || !gameState.isPlayerAlly(evt.from) || !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 = true;
+					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();
+			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 +122,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 +159,7 @@
 					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});
+					Engine.PostCommand(PlayerID, {"type": "aievent", "from": PlayerID, "action": "attack", "target": attack.targetPlayer});
 				}
 				else
 					attack.Abort(gameState);
@@ -129,7 +192,7 @@
 
 	if (this.rushNumber < this.maxRushes && gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_barracks"), true) >= 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] };
@@ -145,7 +208,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 ((gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_barracks"), true) >= 1
@@ -172,7 +235,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 16505)
+++ 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)
 {
Index: binaries/data/mods/public/simulation/ai/petra/chatHelper.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/chatHelper.js	(révision 16505)
+++ binaries/data/mods/public/simulation/ai/petra/chatHelper.js	(copie de travail)
@@ -14,6 +14,29 @@
 	Engine.PostCommand(PlayerID, chat);
 };
 
+m.chatAnswerRequestAttack = function(gameState, player, answer, other)
+{
+	var name = gameState.sharedScript.playersData[player].name;
+	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)
+			var message = "/team " + markForTranslation("I cannot help you against %(name)s for the time being, I have another attack foreseen.");
+		else
+			var message = "/team " + markForTranslation("Sorry, I have not enough soldiers to attack %(name)s right now, but my next attack will target him.");
+	}
+
+	var chat = { "type": "aichat", "message": message, "translateMessage": true, "translateParameters": ["name"], "parameters": { "name": name } };
+	Engine.PostCommand(PlayerID, chat);
+};
+
 m.chatSentTribute = function(gameState, player)
 {
 	var name = gameState.sharedScript.playersData[player].name;
Index: binaries/data/mods/public/simulation/components/AIInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/AIInterface.js	(révision 16505)
+++ binaries/data/mods/public/simulation/components/AIInterface.js	(copie de travail)
@@ -17,7 +17,8 @@
 	"OwnershipChanged",
 	"Garrison",
 	"UnGarrison",
-	"TributeExchanged"
+	"TributeExchanged",
+	"PlayerMessage"
 ];
 
 AIInterface.prototype.Init = function()
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js	(révision 16505)
+++ binaries/data/mods/public/simulation/helpers/Commands.js	(copie de travail)
@@ -644,6 +644,16 @@
 			}
 		}
 	},
+
+	"aievent": function(player, cmd, data)
+	{
+		// Used to allow interaction between players and AIs: i.e. warn it of foreseen attacks
+		let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
+		if (!cmpAIInterface)
+			return;
+		cmpAIInterface.PushEvent("PlayerMessage", cmd);
+	},
+
 	"dialog-answer": function(player, cmd, data)
 	{
 		// Currently nothing. Triggers can read it anyway, and send this
