Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/attack-request.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/attack-request.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/attack-request.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/attack-request.png (revision 16533)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/attack-request.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/diplomacy_window.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/diplomacy_window.xml (revision 16532)
+++ ps/trunk/binaries/data/mods/public/gui/session/diplomacy_window.xml (revision 16533)
@@ -1,76 +1,80 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/menu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 16532)
+++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 16533)
@@ -1,691 +1,700 @@
const PAUSE = translate("Pause");
const RESUME = translate("Resume");
/*
* MENU POSITION CONSTANTS
*/
// Menu / panel border size
const MARGIN = 4;
// Includes the main menu button
const NUM_BUTTONS = 8;
// Regular menu buttons
const BUTTON_HEIGHT = 32;
// The position where the bottom of the menu will end up (currently 228)
const END_MENU_POSITION = (BUTTON_HEIGHT * NUM_BUTTONS) + MARGIN;
// Menu starting position: bottom
const MENU_BOTTOM = 0;
// Menu starting position: top
const MENU_TOP = MENU_BOTTOM - END_MENU_POSITION;
// Menu starting position: overall
const INITIAL_MENU_POSITION = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM;
// Number of pixels per millisecond to move
const MENU_SPEED = 1.2;
// Trade menu: available resources and step for probability changes
const RESOURCES = ["food", "wood", "stone", "metal"];
const STEP = 5;
var isMenuOpen = false;
var menu;
var isDiplomacyOpen = false;
var isTradeOpen = false;
// Redefined every time someone makes a tribute (so we can save some data in a closure). Called in input.js handleInputBeforeGui.
var flushTributing = function() {};
// Ignore size defined in XML and set the actual menu size here
function initMenuPosition()
{
menu = Engine.GetGUIObjectByName("menu");
menu.size = INITIAL_MENU_POSITION;
}
// =============================================================================
// Overall Menu
// =============================================================================
//
// Slide menu
function updateMenuPosition(dt)
{
if (isMenuOpen)
{
var maxOffset = END_MENU_POSITION - menu.size.bottom;
if (maxOffset > 0)
{
var offset = Math.min(MENU_SPEED * dt, maxOffset);
var size = menu.size;
size.top += offset;
size.bottom += offset;
menu.size = size;
}
}
else
{
var maxOffset = menu.size.top - MENU_TOP;
if (maxOffset > 0)
{
var offset = Math.min(MENU_SPEED * dt, maxOffset);
var size = menu.size;
size.top -= offset;
size.bottom -= offset;
menu.size = size;
}
}
}
// Opens the menu by revealing the screen which contains the menu
function openMenu()
{
isMenuOpen = true;
}
// Closes the menu and resets position
function closeMenu()
{
isMenuOpen = false;
}
function toggleMenu()
{
if (isMenuOpen == true)
closeMenu();
else
openMenu();
}
// Menu buttons
// =============================================================================
function optionsMenuButton()
{
closeMenu();
closeOpenDialogs();
openOptions();
}
function chatMenuButton()
{
closeMenu();
closeOpenDialogs();
openChat();
}
function diplomacyMenuButton()
{
closeMenu();
closeOpenDialogs();
openDiplomacy();
}
function pauseMenuButton()
{
togglePause();
}
function resignMenuButton()
{
closeMenu();
closeOpenDialogs();
pauseGame();
var btCaptions = [translate("Yes"), translate("No")];
var btCode = [resignGame, resumeGame];
messageBox(400, 200, translate("Are you sure you want to resign?"), translate("Confirmation"), 0, btCaptions, btCode);
}
function exitMenuButton()
{
closeMenu();
closeOpenDialogs();
pauseGame();
if (g_IsNetworked && g_IsController)
{
var btCode = [resumeGame, leaveGame];
var message = translate("Are you sure you want to quit? Leaving will disconnect all other players.");
}
else if (g_IsNetworked && !g_GameEnded && !g_IsObserver)
{
var btCode = [resumeGame, networkReturnQuestion];
var message = translate("Are you sure you want to quit?");
}
else
{
var btCode = [resumeGame, leaveGame];
var message = translate("Are you sure you want to quit?");
}
messageBox(400, 200, message, translate("Confirmation"), 0, [translate("No"), translate("Yes")], btCode);
}
function networkReturnQuestion()
{
var btCaptions = [translate("I will return"), translate("I resign")];
var btCode = [leaveGame, resignGame];
var btArgs = [true, false];
messageBox(400, 200, translate("Do you want to resign or will you return soon?"), translate("Confirmation"), 0, btCaptions, btCode, btArgs);
}
function openDeleteDialog(selection)
{
closeMenu();
closeOpenDialogs();
var deleteSelectedEntities = function (selectionArg)
{
Engine.PostNetworkCommand({"type": "delete-entities", "entities": selectionArg});
};
var btCaptions = [translate("Yes"), translate("No")];
var btCode = [deleteSelectedEntities, resumeGame];
var btArgs = [selection, null];
messageBox(400, 200, translate("Destroy everything currently selected?"), translate("Delete"), 0, btCaptions, btCode, btArgs);
}
// Menu functions
// =============================================================================
function openSave()
{
closeMenu();
closeOpenDialogs();
pauseGame();
var savedGameData = getSavedGameData();
Engine.PushGuiPage("page_savegame.xml", {"savedGameData":savedGameData, "callback":"resumeGame"});
}
function openOptions()
{
pauseGame();
Engine.PushGuiPage("page_options.xml", {"callback":"resumeGame"});
}
function openChat()
{
updateTeamCheckbox(false);
Engine.GetGUIObjectByName("chatInput").focus(); // Grant focus to the input area
Engine.GetGUIObjectByName("chatDialogPanel").hidden = false;
}
function closeChat()
{
Engine.GetGUIObjectByName("chatInput").caption = ""; // Clear chat input
Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus
Engine.GetGUIObjectByName("chatDialogPanel").hidden = true;
}
function updateTeamCheckbox(check)
{
Engine.GetGUIObjectByName("toggleTeamChatLabel").hidden = g_IsObserver;
let toggleTeamChat = Engine.GetGUIObjectByName("toggleTeamChat");
toggleTeamChat.hidden = g_IsObserver;
toggleTeamChat.checked = !g_IsObserver && check;
}
function toggleChatWindow(teamChat)
{
var chatWindow = Engine.GetGUIObjectByName("chatDialogPanel");
var chatInput = Engine.GetGUIObjectByName("chatInput");
if (chatWindow.hidden)
chatInput.focus(); // Grant focus to the input area
else
{
if (chatInput.caption.length)
{
submitChatInput();
return;
}
chatInput.caption = ""; // Clear chat input
}
updateTeamCheckbox(teamChat);
chatWindow.hidden = !chatWindow.hidden;
}
function setDiplomacy(data)
{
Engine.PostNetworkCommand({"type": "diplomacy", "to": data.to, "player": data.player});
}
function tributeResource(data)
{
Engine.PostNetworkCommand({"type": "tribute", "player": data.player, "amounts": data.amounts});
}
function openDiplomacy()
{
if (isTradeOpen)
closeTrade();
isDiplomacyOpen = true;
let we = Engine.GetPlayerID();
// Get offset for one line
let onesize = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size;
let rowsize = onesize.bottom - onesize.top;
// We don't include gaia
for (let i = 1; i < g_Players.length; ++i)
{
// Apply offset
let row = Engine.GetGUIObjectByName("diplomacyPlayer["+(i-1)+"]");
let size = row.size;
size.top = rowsize*(i-1);
size.bottom = rowsize*i;
row.size = size;
// Set background color
let playerColor = rgbToGuiColor(g_Players[i].color);
row.sprite = "color: "+playerColor + " 32";
Engine.GetGUIObjectByName("diplomacyPlayerName["+(i-1)+"]").caption = "[color=\"" + playerColor + "\"]" + g_Players[i].name + "[/color]";
Engine.GetGUIObjectByName("diplomacyPlayerCiv["+(i-1)+"]").caption = g_CivData[g_Players[i].civ].Name;
Engine.GetGUIObjectByName("diplomacyPlayerTeam["+(i-1)+"]").caption = (g_Players[i].team < 0) ? translateWithContext("team", "None") : g_Players[i].team+1;
if (i != we)
Engine.GetGUIObjectByName("diplomacyPlayerTheirs["+(i-1)+"]").caption = (g_Players[i].isAlly[we] ? translate("Ally") : (g_Players[i].isNeutral[we] ? translate("Neutral") : translate("Enemy")));
// Don't display the options for ourself, or if we or the other player aren't active anymore
if (i == we || g_Players[we].state != "active" || g_Players[i].state != "active")
{
// 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;
}
// Tribute
for each (let resource in ["food", "wood", "stone", "metal"])
{
let button = Engine.GetGUIObjectByName("diplomacyPlayerTribute"+resource[0].toUpperCase()+resource.substring(1)+"["+(i-1)+"]");
button.onpress = (function(player, resource, button){
// Implement something like how unit batch training works. Shift+click to send 500, shift+click+click to send 1000, etc.
// Also see input.js (searching for "INPUT_MASSTRIBUTING" should get all the relevant parts).
let multiplier = 1;
return function() {
let isBatchTrainPressed = Engine.HotkeyIsPressed("session.masstribute");
if (isBatchTrainPressed)
{
inputState = INPUT_MASSTRIBUTING;
multiplier += multiplier == 1 ? 4 : 5;
}
let amounts = {
"food": (resource == "food" ? 100 : 0) * multiplier,
"wood": (resource == "wood" ? 100 : 0) * multiplier,
"stone": (resource == "stone" ? 100 : 0) * multiplier,
"metal": (resource == "metal" ? 100 : 0) * multiplier,
};
button.tooltip = formatTributeTooltip(g_Players[player], resource, amounts[resource]);
// This is in a closure so that we have access to `player`, `amounts`, and `multiplier` without some
// evil global variable hackery.
flushTributing = function() {
tributeResource({"player": player, "amounts": amounts});
multiplier = 1;
button.tooltip = formatTributeTooltip(g_Players[player], resource, 100);
};
if (!isBatchTrainPressed)
flushTributing();
};
})(i, resource, button);
button.hidden = false;
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 = translate("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;
// Diplomacy settings
// Set up the buttons
for each (let setting in ["Ally", "Neutral", "Enemy"])
{
let button = Engine.GetGUIObjectByName("diplomacyPlayer"+setting+"["+(i-1)+"]");
button.caption = g_Players[we]["is"+setting][i] ? translate("x") : "";
button.onpress = (function(e){ return function() { setDiplomacy(e) } })({"player": i, "to": setting.toLowerCase()});
button.hidden = false;
}
}
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false;
}
function closeDiplomacy()
{
isDiplomacyOpen = false;
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = true;
}
function toggleDiplomacy()
{
if (isDiplomacyOpen)
closeDiplomacy();
else
openDiplomacy();
}
function openTrade()
{
if (isDiplomacyOpen)
closeDiplomacy();
isTradeOpen = true;
var updateButtons = function()
{
for (var res in button)
{
button[res].label.caption = proba[res] + "%";
if (res == selec)
{
button[res].sel.hidden = false;
button[res].up.hidden = true;
button[res].dn.hidden = true;
}
else
{
button[res].sel.hidden = true;
button[res].up.hidden = (proba[res] == 100 || proba[selec] == 0);
button[res].dn.hidden = (proba[res] == 0 || proba[selec] == 100);
}
}
}
var proba = Engine.GuiInterfaceCall("GetTradingGoods");
var button = {};
var selec = RESOURCES[0];
for (var i = 0; i < RESOURCES.length; ++i)
{
var buttonResource = Engine.GetGUIObjectByName("tradeResource["+i+"]");
if (i > 0)
{
var size = Engine.GetGUIObjectByName("tradeResource["+(i-1)+"]").size;
var width = size.right - size.left;
size.left += width;
size.right += width;
Engine.GetGUIObjectByName("tradeResource["+i+"]").size = size;
}
var resource = RESOURCES[i];
proba[resource] = (proba[resource] ? proba[resource] : 0);
var buttonResource = Engine.GetGUIObjectByName("tradeResourceButton["+i+"]");
var icon = Engine.GetGUIObjectByName("tradeResourceIcon["+i+"]");
icon.sprite = "stretched:session/icons/resources/" + resource + ".png";
var label = Engine.GetGUIObjectByName("tradeResourceText["+i+"]");
var buttonUp = Engine.GetGUIObjectByName("tradeArrowUp["+i+"]");
var buttonDn = Engine.GetGUIObjectByName("tradeArrowDn["+i+"]");
var iconSel = Engine.GetGUIObjectByName("tradeResourceSelection["+i+"]");
button[resource] = { "up": buttonUp, "dn": buttonDn, "label": label, "sel": iconSel };
buttonResource.onpress = (function(resource){
return function() {
if (Engine.HotkeyIsPressed("session.fulltradeswap"))
{
for (var ress of RESOURCES)
proba[ress] = 0;
proba[resource] = 100;
Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba});
}
selec = resource;
updateButtons();
}
})(resource);
buttonUp.onpress = (function(resource){
return function() {
proba[resource] += Math.min(STEP, proba[selec]);
proba[selec] -= Math.min(STEP, proba[selec]);
Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba});
updateButtons();
}
})(resource);
buttonDn.onpress = (function(resource){
return function() {
proba[selec] += Math.min(STEP, proba[resource]);
proba[resource] -= Math.min(STEP, proba[resource]);
Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba});
updateButtons();
}
})(resource);
}
updateButtons();
var traderNumber = Engine.GuiInterfaceCall("GetTraderNumber");
var caption = "";
if (traderNumber.landTrader.total == 0)
caption = translate("There are no land traders.");
else
{
var inactive = traderNumber.landTrader.total - traderNumber.landTrader.trading - traderNumber.landTrader.garrisoned;
var inactiveString = "";
if (inactive > 0)
inactiveString = "[color=\"orange\"]" + sprintf(translatePlural("%(numberOfLandTraders)s inactive", "%(numberOfLandTraders)s inactive", inactive), { numberOfLandTraders: inactive }) + "[/color]";
if (traderNumber.landTrader.trading > 0)
{
var openingTradingString = sprintf(translatePlural("There is %(numberTrading)s land trader trading", "There are %(numberTrading)s land traders trading", traderNumber.landTrader.trading), { numberTrading: traderNumber.landTrader.trading });
if (traderNumber.landTrader.garrisoned > 0)
{
var garrisonedString = sprintf(translatePlural("%(numberGarrisoned)s garrisoned on a trading merchant ship", "%(numberGarrisoned)s garrisoned on a trading merchant ship", traderNumber.landTrader.garrisoned), { numberGarrisoned: traderNumber.landTrader.garrisoned });
if (inactive > 0)
{
caption = sprintf(translate("%(openingTradingString)s, %(garrisonedString)s, and %(inactiveString)s."), {
openingTradingString: openingTradingString,
garrisonedString: garrisonedString,
inactiveString: inactiveString
});
}
else
{
caption = sprintf(translate("%(openingTradingString)s, and %(garrisonedString)s."), {
openingTradingString: openingTradingString,
garrisonedString: garrisonedString
});
}
}
else
{
if (inactive > 0)
{
caption = sprintf(translate("%(openingTradingString)s, and %(inactiveString)s."), {
openingTradingString: openingTradingString,
inactiveString: inactiveString
});
}
else
{
caption = sprintf(translate("%(openingTradingString)s."), {
openingTradingString: openingTradingString,
});
}
}
}
else
{
if (traderNumber.landTrader.garrisoned > 0)
{
var openingGarrisonedString = sprintf(translatePlural("There is %(numberGarrisoned)s land trader garrisoned on a trading merchant ship", "There are %(numberGarrisoned)s land traders garrisoned on a trading merchant ship", traderNumber.landTrader.garrisoned), { numberGarrisoned: traderNumber.landTrader.garrisoned });
if (inactive > 0)
{
caption = sprintf(translate("%(openingGarrisonedString)s, and %(inactiveString)s."), {
openingGarrisonedString: openingGarrisonedString,
inactiveString: inactiveString
});
}
else
{
caption = sprintf(translate("%(openingGarrisonedString)s."), {
openingGarrisonedString: openingGarrisonedString
});
}
}
else
{
if (inactive > 0)
{
inactiveString = "[color=\"orange\"]" + sprintf(translatePlural("%(numberOfLandTraders)s land trader inactive", "%(numberOfLandTraders)s land traders inactive", inactive), { numberOfLandTraders: inactive }) + "[/color]";
caption = sprintf(translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive), {
inactiveString: inactiveString
});
}
// The “else” here is already handled by “if (traderNumber.landTrader.total == 0)” above.
}
}
}
Engine.GetGUIObjectByName("landTraders").caption = caption;
caption = "";
if (traderNumber.shipTrader.total == 0)
caption = translate("There are no merchant ships.");
else
{
var inactive = traderNumber.shipTrader.total - traderNumber.shipTrader.trading;
var inactiveString = "";
if (inactive > 0)
inactiveString = "[color=\"orange\"]" + sprintf(translatePlural("%(numberOfShipTraders)s inactive", "%(numberOfShipTraders)s inactive", inactive), { numberOfShipTraders: inactive }) + "[/color]";
if (traderNumber.shipTrader.trading > 0)
{
var openingTradingString = sprintf(translatePlural("There is %(numberTrading)s merchant ship trading", "There are %(numberTrading)s merchant ships trading", traderNumber.shipTrader.trading), { numberTrading: traderNumber.shipTrader.trading });
if (inactive > 0)
{
caption = sprintf(translate("%(openingTradingString)s, and %(inactiveString)s."), {
openingTradingString: openingTradingString,
inactiveString: inactiveString
});
}
else
{
caption = sprintf(translate("%(openingTradingString)s."), {
openingTradingString: openingTradingString,
});
}
}
else
{
if (inactive > 0)
{
inactiveString = "[color=\"orange\"]" + sprintf(translatePlural("%(numberOfShipTraders)s merchant ship inactive", "%(numberOfShipTraders)s merchant ships inactive", inactive), { numberOfShipTraders: inactive }) + "[/color]";
caption = sprintf(translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive), {
inactiveString: inactiveString
});
}
// The “else” here is already handled by “if (traderNumber.shipTrader.total == 0)” above.
}
}
Engine.GetGUIObjectByName("shipTraders").caption = caption;
Engine.GetGUIObjectByName("tradeDialogPanel").hidden = false;
}
function closeTrade()
{
isTradeOpen = false;
Engine.GetGUIObjectByName("tradeDialogPanel").hidden = true;
}
function toggleTrade()
{
if (isTradeOpen)
closeTrade();
else
openTrade();
}
function toggleGameSpeed()
{
var gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.hidden = !gameSpeed.hidden;
}
/**
* Pause the game in single player mode.
*/
function pauseGame()
{
if (g_IsNetworked)
return;
Engine.GetGUIObjectByName("pauseButtonText").caption = RESUME;
Engine.GetGUIObjectByName("pauseOverlay").hidden = false;
Engine.SetPaused(true);
}
function resumeGame()
{
Engine.GetGUIObjectByName("pauseButtonText").caption = PAUSE;
Engine.GetGUIObjectByName("pauseOverlay").hidden = true;
Engine.SetPaused(false);
}
function togglePause()
{
closeMenu();
closeOpenDialogs();
var pauseOverlay = Engine.GetGUIObjectByName("pauseOverlay");
if (pauseOverlay.hidden)
{
Engine.GetGUIObjectByName("pauseButtonText").caption = RESUME;
Engine.SetPaused(true);
}
else
{
Engine.SetPaused(false);
Engine.GetGUIObjectByName("pauseButtonText").caption = PAUSE;
}
pauseOverlay.hidden = !pauseOverlay.hidden;
}
function openManual()
{
closeMenu();
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_manual.xml", {"page": "manual/intro", "title":translate("Manual"), "url":"http://trac.wildfiregames.com/wiki/0adManual", "callback": "resumeGame"});
}
function toggleDeveloperOverlay()
{
// The developer overlay is disabled in ranked games
if (Engine.HasXmppClient() && Engine.IsRankedGame())
return;
var devCommands = Engine.GetGUIObjectByName("devCommands");
if (devCommands.hidden)
submitChatDirectly(translate("The Developer Overlay was opened."));
else
submitChatDirectly(translate("The Developer Overlay was closed."));
// Toggle the overlay
devCommands.hidden = !devCommands.hidden;
}
function closeOpenDialogs()
{
closeMenu();
closeChat();
closeDiplomacy();
closeTrade();
}
function formatTributeTooltip(player, resource, amount)
{
let playerColor = rgbToGuiColor(player.color);
return sprintf(translate("Tribute %(resourceAmount)s %(resourceType)s to %(playerName)s. Shift-click to tribute %(greaterAmount)s."), {
resourceAmount: amount,
resourceType: getLocalizedResourceName(resource, "withinSentence"),
playerName: "[color=\"" + playerColor + "\"]" + player.name + "[/color]",
greaterAmount: (amount < 500 ? 500 : amount + 500)
});
}
Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 16532)
+++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 16533)
@@ -1,732 +1,740 @@
// Chat data
const CHAT_TIMEOUT = 30000;
const MAX_NUM_CHAT_LINES = 20;
var chatMessages = [];
var chatTimers = [];
// Notification Data
const NOTIFICATION_TIMEOUT = 10000;
const MAX_NUM_NOTIFICATION_LINES = 3;
var notifications = [];
var notificationsTimers = [];
var cheats = getCheatsData();
function getCheatsData()
{
var cheats = {};
var cheatFileList = getJSONFileList("simulation/data/cheats/");
for each (var fileName in cheatFileList)
{
var currentCheat = Engine.ReadJSONFile("simulation/data/cheats/"+fileName+".json");
if (!currentCheat)
continue;
if (Object.keys(cheats).indexOf(currentCheat.Name) !== -1)
warn("Cheat name '" + currentCheat.Name + "' is already present");
else
cheats[currentCheat.Name] = currentCheat.Data;
}
return cheats;
}
var g_NotificationsTypes =
{
"chat": function(notification, player)
{
var message = {
"type": "message",
"text": notification.message
}
var guid = findGuidForPlayerID(g_PlayerAssignments, player);
if (guid == undefined)
{
message["guid"] = -1;
message["player"] = player;
} else {
message["guid"] = guid;
}
addChatMessage(message);
},
"aichat": function(notification, player)
{
var message = {
"type": "message",
"text": notification.message
}
message["translate"] = true;
if ("translateParameters" in notification)
{
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 (!param.startsWith("_player_"))
+ continue;
+ let colorName = getUsernameAndColor(+message["parameters"][param]);
+ message["parameters"][param] = "[color=\"" + colorName[1] + "\"]" + colorName[0] + "[/color]";
+ }
}
var guid = findGuidForPlayerID(g_PlayerAssignments, player);
if (guid == undefined)
{
message["guid"] = -1;
message["player"] = player;
} else {
message["guid"] = guid;
}
addChatMessage(message);
},
"defeat": function(notification, player)
{
addChatMessage({
"type": "defeat",
"guid": findGuidForPlayerID(g_PlayerAssignments, player),
"player": player
});
updateDiplomacy();
},
"diplomacy": function(notification, player)
{
addChatMessage({
"type": "diplomacy",
"player": player,
"player1": notification.player1,
"status": notification.status
});
updateDiplomacy();
},
"quit": function(notification, player)
{
Engine.Exit();
},
"tribute": function(notification, player)
{
addChatMessage({
"type": "tribute",
"player": player,
"player1": notification.donator,
"amounts": notification.amounts
});
},
"attack": function(notification, player)
{
if (player != Engine.GetPlayerID())
return;
if (Engine.ConfigDB_GetValue("user", "gui.session.attacknotificationmessage") !== "true")
return;
addChatMessage({
"type": "attack",
"player": player,
"attacker": notification.attacker
});
},
"dialog": function(notification, player)
{
if (player == Engine.GetPlayerID())
openDialog(notification.dialogName, notification.data, player);
},
"resetselectionpannel": function(notification, player)
{
if (player != Engine.GetPlayerID())
return;
g_Selection.rebuildSelection([]);
}
};
// Notifications
function handleNotifications()
{
var notification = Engine.GuiInterfaceCall("GetNextNotification");
if (!notification)
return;
if (!notification.type)
{
error("notification without type found.\n"+uneval(notification))
return;
}
if (!notification.players)
{
error("notification without players found.\n"+uneval(notification))
return;
}
var action = g_NotificationsTypes[notification.type];
if (!action)
{
error("unknown notification type '" + notification.type + "' found.");
return;
}
for (var player of notification.players)
action(notification, player);
}
function updateDiplomacy()
{
g_Players = getPlayerData(g_PlayerAssignments);
// If the diplomacy panel is open refresh it.
if (isDiplomacyOpen)
openDiplomacy();
}
function updateTimeNotifications()
{
var notifications = Engine.GuiInterfaceCall("GetTimeNotifications");
var notificationText = "";
var playerID = Engine.GetPlayerID();
for (var n of notifications)
{
if (!n.players)
{
warn("notification has unknown player list. Text:\n"+n.message);
continue;
}
if (n.players.indexOf(playerID) == -1)
continue;
var message = n.message;
if (n.translateMessage)
message = translate(message);
var parameters = n.parameters || {};
if (n.translateParameters)
translateObjectKeys(parameters, n.translateParameters);
parameters.time = timeToString(n.time);
notificationText += sprintf(message, parameters) + "\n";
}
Engine.GetGUIObjectByName("notificationText").caption = notificationText;
}
// Returns [username, playercolor] for the given player
function getUsernameAndColor(player)
{
// This case is hit for AIs, whose names don't exist in playerAssignments.
var color = g_Players[player].color;
return [
escapeText(g_Players[player].name),
color.r + " " + color.g + " " + color.b,
];
}
// Messages
function handleNetMessage(message)
{
log(sprintf(translate("Net message: %(message)s"), { message: uneval(message) }));
switch (message.type)
{
case "netstatus":
// If we lost connection, further netstatus messages are useless
if (g_Disconnected)
return;
var obj = Engine.GetGUIObjectByName("netStatus");
switch (message.status)
{
case "waiting_for_players":
obj.caption = translate("Waiting for other players to connect...");
obj.hidden = false;
break;
case "join_syncing":
obj.caption = translate("Synchronising gameplay with other players...");
obj.hidden = false;
break;
case "active":
obj.caption = "";
obj.hidden = true;
break;
case "connected":
obj.caption = translate("Connected to the server.");
obj.hidden = false;
break;
case "authenticated":
obj.caption = translate("Connection to the server has been authenticated.");
obj.hidden = false;
break;
case "disconnected":
g_Disconnected = true;
obj.caption = translate("Connection to the server has been lost.") + "\n\n" + translate("The game has ended.");
obj.hidden = false;
break;
default:
error("Unrecognised netstatus type '" + message.status + "'");
break;
}
break;
case "players":
// Find and report all leavings
for (var host in g_PlayerAssignments)
{
if (! message.hosts[host])
{
// Tell the user about the disconnection
addChatMessage({ "type": "disconnect", "guid": host });
// Update the cached player data, so we can display the disconnection status
updatePlayerDataRemove(g_Players, host);
}
}
// Find and report all joinings
for (var host in message.hosts)
{
if (! g_PlayerAssignments[host])
{
// Update the cached player data, so we can display the correct name
updatePlayerDataAdd(g_Players, host, message.hosts[host]);
// Tell the user about the connection
addChatMessage({ "type": "connect", "guid": host }, message.hosts);
}
}
g_PlayerAssignments = message.hosts;
if (g_IsController && Engine.HasXmppClient())
{
var players = [ assignment.name for each (assignment in g_PlayerAssignments) ]
Engine.SendChangeStateGame(Object.keys(g_PlayerAssignments).length, players.join(", "));
}
break;
case "chat":
addChatMessage({ "type": "message", "guid": message.guid, "text": message.text });
break;
case "aichat":
addChatMessage({ "type": "message", "guid": message.guid, "text": message.text, "translate": true });
break;
// To prevent errors, ignore these message types that occur during autostart
case "gamesetup":
case "start":
break;
default:
error("Unrecognised net message type '" + message.type + "'");
}
}
function submitChatDirectly(text)
{
if (text.length)
{
if (g_IsNetworked)
Engine.SendNetworkChat(text);
else
addChatMessage({ "type": "message", "guid": "local", "text": text });
}
}
function submitChatInput()
{
var input = Engine.GetGUIObjectByName("chatInput");
var text = input.caption;
var isCheat = false;
if (text.length)
{
if (!g_IsObserver && g_Players[Engine.GetPlayerID()].cheatsEnabled)
{
for each (var cheat in Object.keys(cheats))
{
// Line must start with the cheat.
if (text.indexOf(cheat) !== 0)
continue;
// test for additional parameter which is the rest of the string after the cheat
var parameter = "";
if (cheats[cheat].DefaultParameter !== undefined)
{
var par = text.substr(cheat.length);
par = par.replace(/^\W+/, '').replace(/\W+$/, ''); // remove whitespaces at start and end
// check, if the isNumeric flag is set
if (cheats[cheat].isNumeric)
{
// Match the first word in the substring.
var match = par.match(/\S+/);
if (match && match[0])
par = Math.floor(match[0]);
// check, if valid number could be parsed
if (par <= 0 || isNaN(par))
par = "";
}
// replace default parameter, if not empty or number
if (par.length > 0 || parseFloat(par) === par)
parameter = par;
else
parameter = cheats[cheat].DefaultParameter;
}
Engine.PostNetworkCommand({
"type": "cheat",
"action": cheats[cheat].Action,
"parameter": parameter,
"text": cheats[cheat].Type,
"selected": g_Selection.toList(),
"templates": cheats[cheat].Templates,
"player": Engine.GetPlayerID()});
isCheat = true;
break;
}
}
// Observers should only send messages to "/all"
if (!isCheat && (!g_IsObserver || text.indexOf("/") == -1 || text.indexOf("/all ") == 0))
{
if (Engine.GetGUIObjectByName("toggleTeamChat").checked)
text = "/team " + text;
if (g_IsNetworked)
Engine.SendNetworkChat(text);
else
addChatMessage({ "type": "message", "guid": "local", "text": text });
}
input.caption = ""; // Clear chat input
}
input.blur(); // Remove focus
toggleChatWindow();
}
function addChatMessage(msg, playerAssignments)
{
// Default to global assignments, but allow overriding for when reporting
// new players joining
if (!playerAssignments)
playerAssignments = g_PlayerAssignments;
var playerColor, username;
// No context by default. May be set by parseChatCommands().
msg.context = "";
if ("guid" in msg && playerAssignments[msg.guid])
{
var n = playerAssignments[msg.guid].player;
// Observers have an ID of -1 which is not a valid index.
if (n < 0)
n = 0;
playerColor = g_Players[n].color.r + " " + g_Players[n].color.g + " " + g_Players[n].color.b;
username = escapeText(playerAssignments[msg.guid].name);
// Parse in-line commands in regular messages.
if (msg.type == "message")
parseChatCommands(msg, playerAssignments);
}
else if (msg.type == "defeat" && msg.player)
{
[username, playerColor] = getUsernameAndColor(msg.player);
}
else if (msg.type == "message")
{
[username, playerColor] = getUsernameAndColor(msg.player);
parseChatCommands(msg, playerAssignments);
}
else
{
playerColor = "255 255 255";
username = translate("Unknown player");
}
var formatted;
switch (msg.type)
{
case "connect":
formatted = sprintf(translate("%(player)s has joined the game."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
break;
case "disconnect":
formatted = sprintf(translate("%(player)s has left the game."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
break;
case "defeat":
// In singleplayer, the local player is "You". "You has" is incorrect.
if (!g_IsNetworked && msg.player == Engine.GetPlayerID())
formatted = translate("You have been defeated.");
else
formatted = sprintf(translate("%(player)s has been defeated."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
break;
case "diplomacy":
var status = (msg.status == "ally" ? "allied" : (msg.status == "enemy" ? "at war" : "neutral"));
if (msg.player == Engine.GetPlayerID())
{
[username, playerColor] = getUsernameAndColor(msg.player1);
if (msg.status == "ally")
formatted = sprintf(translate("You are now allied with %(player)s."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
else if (msg.status == "enemy")
formatted = sprintf(translate("You are now at war with %(player)s."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
else // (msg.status == "neutral")
formatted = sprintf(translate("You are now neutral with %(player)s."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
}
else if (msg.player1 == Engine.GetPlayerID())
{
[username, playerColor] = getUsernameAndColor(msg.player);
if (msg.status == "ally")
formatted = sprintf(translate("%(player)s is now allied with you."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
else if (msg.status == "enemy")
formatted = sprintf(translate("%(player)s is now at war with you."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
else // (msg.status == "neutral")
formatted = sprintf(translate("%(player)s is now neutral with you."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
}
else // No need for other players to know of this.
return;
break;
case "tribute":
if (msg.player != Engine.GetPlayerID())
return;
[username, playerColor] = getUsernameAndColor(msg.player1);
// Format the amounts to proper English: 200 food, 100 wood, and 300 metal; 100 food; 400 wood and 200 stone
let amounts = Object.keys(msg.amounts)
.filter(function (type) { return msg.amounts[type] > 0; })
.map(function (type) { return sprintf(translate("%(amount)s %(resourceType)s"), {
"amount": msg.amounts[type],
"resourceType": getLocalizedResourceName(type, "withinSentence")});
});
if (amounts.length > 1)
{
let lastAmount = amounts.pop();
amounts = sprintf(translate("%(previousAmounts)s and %(lastAmount)s"), {
previousAmounts: amounts.join(translate(", ")),
lastAmount: lastAmount
});
}
formatted = sprintf(translate("%(player)s has sent you %(amounts)s."), {
player: "[color=\"" + playerColor + "\"]" + username + "[/color]",
amounts: amounts
});
break;
case "attack":
if (msg.player != Engine.GetPlayerID())
return;
[username, playerColor] = getUsernameAndColor(msg.attacker);
formatted = sprintf(translate("You have been attacked by %(attacker)s!"), { attacker: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
break;
case "message":
// May have been hidden by the 'team' command.
if (msg.hide)
return;
var message;
if ("translate" in msg && msg.translate)
{
message = translate(msg.text); // No need to escape, not a user message.
if ("translateParameters" in msg && msg.translateParameters)
{
var parameters = msg.parameters || {};
translateObjectKeys(parameters, msg.translateParameters);
message = sprintf(message, parameters);
}
}
else
message = escapeText(msg.text);
if (msg.action)
{
if (msg.context !== "")
{
formatted = sprintf(translate("(%(context)s) * %(user)s %(message)s"), {
context: msg.context,
user: "[color=\"" + playerColor + "\"]" + username + "[/color]",
message: message
});
}
else
{
formatted = sprintf(translate("* %(user)s %(message)s"), {
user: "[color=\"" + playerColor + "\"]" + username + "[/color]",
message: message
});
}
}
else
{
var userTag = sprintf(translate("<%(user)s>"), { user: username })
var formattedUserTag = sprintf(translate("<%(user)s>"), { user: "[color=\"" + playerColor + "\"]" + username + "[/color]" })
if (msg.context !== "")
{
formatted = sprintf(translate("(%(context)s) %(userTag)s %(message)s"), {
context: msg.context,
userTag: formattedUserTag,
message: message
});
}
else
{
formatted = sprintf(translate("%(userTag)s %(message)s"), { userTag: formattedUserTag, message: message});
}
}
break;
default:
error(sprintf("Invalid chat message '%(message)s'", { message: uneval(msg) }));
return;
}
chatMessages.push(formatted);
chatTimers.push(setTimeout(removeOldChatMessages, CHAT_TIMEOUT));
if (chatMessages.length > MAX_NUM_CHAT_LINES)
removeOldChatMessages();
else
Engine.GetGUIObjectByName("chatText").caption = chatMessages.join("\n");
}
function removeOldChatMessages()
{
clearTimeout(chatTimers[0]); // The timer only needs to be cleared when new messages bump old messages off
chatTimers.shift();
chatMessages.shift();
Engine.GetGUIObjectByName("chatText").caption = chatMessages.join("\n");
}
// Parses chat messages for commands.
function parseChatCommands(msg, playerAssignments)
{
// Only interested in messages that start with '/'.
if (!msg.text || msg.text[0] != '/')
return;
var sender;
if (playerAssignments[msg.guid])
sender = playerAssignments[msg.guid].player;
else
sender = msg.player;
// TODO: It would be nice to display multiple different contexts.
// It should be made clear that only players matching the union of those receive the message.
var recurse = false;
var split = msg.text.split(/\s/);
// Parse commands embedded in the message.
switch (split[0])
{
case "/all":
// Resets values that 'team' or 'enemy' may have set.
msg.context = "";
msg.hide = false;
recurse = true;
break;
case "/team":
// Check if we are in a team.
if (g_Players[Engine.GetPlayerID()] && g_Players[Engine.GetPlayerID()].team != -1)
{
if (g_Players[Engine.GetPlayerID()].team != g_Players[sender].team)
msg.hide = true;
else
msg.context = translate("Team");
}
else
msg.hide = true;
recurse = true;
break;
case "/ally":
case "/allies":
// Check if we sent the message, or are the sender's (mutual) ally
if (Engine.GetPlayerID() == sender || (g_Players[sender] && g_Players[sender].isMutualAlly[Engine.GetPlayerID()]))
msg.context = translate("Ally");
else
msg.hide = true;
recurse = true;
break;
case "/enemy":
case "/enemies":
// Check if we sent the message, or are the sender's enemy
if (Engine.GetPlayerID() == sender || (g_Players[sender] && g_Players[sender].isEnemy[Engine.GetPlayerID()]))
msg.context = translate("Enemy");
else
msg.hide = true;
recurse = true;
break;
case "/me":
msg.action = true;
break;
case "/msg":
var trimmed = msg.text.substr(split[0].length + 1);
var matched = "";
// Reject names which don't match or are a superset of the intended name.
for each (var player in playerAssignments)
if (trimmed.indexOf(player.name + " ") == 0 && player.name.length > matched.length)
matched = player.name;
// If the local player's name was the longest one matched, show the message.
var playerName = g_Players[Engine.GetPlayerID()].name;
if (matched.length && (matched == playerName || sender == Engine.GetPlayerID()))
{
msg.context = translate("Private");
msg.text = trimmed.substr(matched.length + 1);
msg.hide = false; // Might override team message hiding.
return;
}
else
msg.hide = true;
break;
default:
return;
}
msg.text = msg.text.substr(split[0].length + 1);
// Hide the message if parsing commands left it empty.
if (!msg.text.length)
msg.hide = true;
// Attempt to parse more commands if the current command allows it.
if (recurse)
parseChatCommands(msg, playerAssignments);
}
function sendDialogAnswer(guiObject, dialogName)
{
Engine.GetGUIObjectByName(dialogName+"-dialog").hidden = true;
Engine.PostNetworkCommand({
"type": "dialog-answer",
"dialog": dialogName,
"answer": guiObject.name.split("-").pop(),
});
resumeGame();
}
function openDialog(dialogName, data, player)
{
var dialog = Engine.GetGUIObjectByName(dialogName+"-dialog");
if (!dialog)
{
warn("messages.js: Unknow dialog with name "+dialogName);
return;
}
dialog.hidden = false;
for (var objName in data)
{
var obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName);
if (!obj)
{
warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog.");
continue;
}
for (var key in data[objName])
{
var n = data[objName][key];
if (typeof n == "object" && n.message)
{
var message = n.message;
if (n.translateMessage)
message = translate(message);
var parameters = n.parameters || {};
if (n.translateParameters)
translateObjectKeys(parameters, n.translateParameters);
obj[key] = sprintf(message, parameters);
}
else
obj[key] = n;
}
}
pauseGame();
}
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 16532)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 16533)
@@ -1,448 +1,516 @@
var PETRA = function(m)
{
/* Attack Manager
*/
m.AttackManager = function(Config)
{
this.Config = Config;
this.totalNumber = 0;
this.attackNumber = 0;
this.rushNumber = 0;
this.raidNumber = 0;
this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] };
this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] };
this.debugTime = 0;
this.maxRushes = 0;
this.rushSize = [];
this.currentEnemyPlayer = undefined; // enemy player we are currently targeting
};
// More initialisation for stuff that needs the gameState
m.AttackManager.prototype.init = function(gameState)
{
this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1));
this.outOfPlan.registerUpdates();
};
m.AttackManager.prototype.setRushes = function()
{
if (this.Config.personality.aggressive > 0.8)
{
this.maxRushes = 3
this.rushSize = [ 16, 22, 28 ];
}
else if (this.Config.personality.aggressive > 0.6)
{
this.maxRushes = 2;
this.rushSize = [ 18, 28 ];
}
else if (this.Config.personality.aggressive > 0.3)
{
this.maxRushes = 1;
this.rushSize = [ 24 ];
}
};
+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;
+ }
+ attack.targetPos = attack.target.position();
+ }
+ 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)
{
if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60)
{
this.debugTime = gameState.ai.elapsedTime;
API3.warn(" upcoming attacks =================");
for (var attackType in this.upcomingAttacks)
for (let attack of this.upcomingAttacks[attackType])
API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length);
API3.warn(" started attacks ==================");
for (var attackType in this.startedAttacks)
for (let attack of this.startedAttacks[attackType])
API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length);
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)
{
if (this.Config.debug > 1)
API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted.");
attack.Abort(gameState);
this.upcomingAttacks[attackType].splice(i--,1);
}
else if (updateStep === 2)
{
if (attack.StartAttack(gameState))
{
if (this.Config.debug > 1)
API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName());
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);
this.upcomingAttacks[attackType].splice(i--,1);
}
}
}
for (var attackType in this.startedAttacks)
{
for (var i = 0; i < this.startedAttacks[attackType].length; ++i)
{
var attack = this.startedAttacks[attackType][i];
attack.checkEvents(gameState, events);
// okay so then we'll update the attack.
if (attack.isPaused())
continue;
var remaining = attack.update(gameState, events);
if (!remaining)
{
if (this.Config.debug > 1)
API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining);
attack.Abort(gameState);
this.startedAttacks[attackType].splice(i--,1);
}
}
}
// creating plans after updating because an aborted plan might be reused in that case.
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] };
var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data);
if (!attackPlan.failed)
{
if (this.Config.debug > 1)
API3.warn("Headquarters: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes);
this.totalNumber++;
attackPlan.init(gameState);
this.upcomingAttacks["Rush"].push(attackPlan);
}
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())))
|| !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack
{
if (this.attackNumber < 2 || this.startedAttacks["HugeAttack"].length > 0)
var type = "Attack";
else
var type = "HugeAttack";
var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, type);
if (attackPlan.failed)
this.attackPlansEncounteredWater = true; // hack
else
{
if (this.Config.debug > 1)
API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber);
this.totalNumber++;
attackPlan.init(gameState);
this.upcomingAttacks[type].push(attackPlan);
}
this.attackNumber++;
}
}
- 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)
{
target = gameState.getEntityById(targetId);
if (target)
break;
}
if (target)
{
// prepare a raid against this target
var data = { "target": target };
var attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data);
if (!attackPlan.failed)
{
if (this.Config.debug > 1)
API3.warn("Headquarters: Raiding plan " + this.totalNumber);
this.totalNumber++;
attackPlan.init(gameState);
this.upcomingAttacks["Raid"].push(attackPlan);
}
this.raidNumber++;
}
}
};
m.AttackManager.prototype.getPlan = function(planName)
{
for (var attackType in this.upcomingAttacks)
{
for (var attack of this.upcomingAttacks[attackType])
if (attack.getName() == planName)
return attack;
}
for (var attackType in this.startedAttacks)
{
for (var attack of this.startedAttacks[attackType])
if (attack.getName() == planName)
return attack;
}
return undefined;
};
m.AttackManager.prototype.pausePlan = function(planName)
{
var attack = this.getPlan(planName);
if (attack)
attack.setPaused(true);
};
m.AttackManager.prototype.unpausePlan = function(planName)
{
var attack = this.getPlan(planName);
if (attack)
attack.setPaused(falsee);
};
m.AttackManager.prototype.pauseAllPlans = function()
{
for (var attackType in this.upcomingAttacks)
for (var attack of this.upcomingAttacks[attackType])
attack.setPaused(true);
for (var attackType in this.startedAttacks)
for (var attack of this.startedAttacks[attackType])
attack.setPaused(true);
};
m.AttackManager.prototype.unpauseAllPlans = function()
{
for (var attackType in this.upcomingAttacks)
for (var attack of this.upcomingAttacks[attackType])
attack.setPaused(false);
for (var attackType in this.startedAttacks)
for (var attack of this.startedAttacks[attackType])
attack.setPaused(false);
};
m.AttackManager.prototype.getAttackInPreparation = function(type)
{
if (!this.upcomingAttacks[type].length)
return undefined;
return this.upcomingAttacks[type][0];
};
/**
* determine which player should be attacked:
* - when called when starting the attack, attack.targetPlayer is undefined
*/
m.AttackManager.prototype.getEnemyPlayer = function(gameState, attack)
{
var enemyPlayer = undefined;
// first check if there is a preferred enemy based on our victory conditions
if (gameState.getGameType() === "wonder")
{
var moreAdvanced = undefined;
var enemyWonder = undefined;
var wonders = gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder"));
for (let wonder of wonders.values())
{
let progress = wonder.foundationProgress();
if (progress === undefined)
return wonder.owner();
if (enemyWonder && moreAdvanced > progress)
continue;
enemyWonder = wonder;
moreAdvanced = progress;
}
if (enemyWonder)
{
enemyPlayer = enemyWonder.owner();
if (attack.targetPlayer === undefined)
this.currentEnemyPlayer = enemyPlayer;
return enemyPlayer;
}
}
// No rush if enemy too well defended (i.e. iberians)
var veto = {};
if (attack.type === "Rush")
{
for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
{
if (!gameState.isPlayerEnemy(i))
continue;
let enemyDefense = 0;
for (let ent of gameState.getEnemyStructures(i).values())
if (ent.hasClass("Tower") || ent.hasClass("Fortress"))
enemyDefense++;
if (enemyDefense > 6)
veto[i] = true;
}
}
// then if not a huge attack, continue attacking our previous target as long as it has some entities,
// otherwise target the most accessible one
if (attack.type !== "HugeAttack")
{
if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined
&& gameState.getEnemyEntities(this.currentEnemyPlayer) > 0)
return this.currentEnemyPlayer;
let distmin = undefined;
let ccmin = undefined;
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
for (let ourcc of ccEnts.values())
{
if (ourcc.owner() !== PlayerID)
continue;
let ourPos = ourcc.position();
let access = gameState.ai.accessibility.getAccessValue(ourPos);
for (let enemycc of ccEnts.values())
{
if (veto[enemycc.owner()])
continue;
if (!gameState.isPlayerEnemy(enemycc.owner()))
continue;
let enemyPos = enemycc.position();
if (access !== gameState.ai.accessibility.getAccessValue(enemyPos))
continue;
let dist = API3.SquareVectorDistance(ourPos, enemyPos);
if (distmin && dist > distmin)
continue;
ccmin = enemycc;
distmin = dist;
}
}
if (ccmin)
{
enemyPlayer = ccmin.owner();
if (attack.targetPlayer === undefined)
this.currentEnemyPlayer = enemyPlayer;
return enemyPlayer;
}
}
// then let's target our strongest enemy (basically counting enemies units)
// with priority to enemies with civ center
var max = 0;
for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
{
if (veto[i])
continue;
if (!gameState.isPlayerEnemy(i))
continue;
let enemyCount = 0;
let enemyCivCentre = false;
for (let ent of gameState.getEnemyEntities(i).values())
{
enemyCount++;
if (ent.hasClass("CivCentre"))
enemyCivCentre = true;
}
if (enemyCivCentre)
enemyCount += 500;
if (enemyCount < max)
continue;
max = enemyCount;
enemyPlayer = i;
}
if (attack.targetPlayer === undefined)
this.currentEnemyPlayer = enemyPlayer;
return enemyPlayer;
};
m.AttackManager.prototype.Serialize = function()
{
let properties = {
"totalNumber": this.totalNumber,
"attackNumber": this.attackNumber,
"rushNumber": this.rushNumber,
"raidNumber": this.raidNumber,
"debugTime": this.debugTime,
"maxRushes": this.maxRushes,
"rushSize": this.rushSize,
"currentEnemyPlayer": this.currentEnemyPlayer
};
let upcomingAttacks = {};
for (let key in this.upcomingAttacks)
{
upcomingAttacks[key] = [];
for (let attack of this.upcomingAttacks[key])
upcomingAttacks[key].push(attack.Serialize());
};
let startedAttacks = {};
for (let key in this.startedAttacks)
{
startedAttacks[key] = [];
for (let attack of this.startedAttacks[key])
startedAttacks[key].push(attack.Serialize());
};
return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks };
};
m.AttackManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
this.upcomingAttacks = {};
for (let key in data.upcomingAttacks)
{
this.upcomingAttacks[key] = [];
for (let dataAttack of data.upcomingAttacks[key])
{
let attack = new m.AttackPlan(gameState, this.Config, 0);
attack.Deserialize(gameState, dataAttack);
attack.init(gameState);
this.upcomingAttacks[key].push(attack);
}
};
this.startedAttacks = {};
for (let key in data.startedAttacks)
{
this.startedAttacks[key] = [];
for (let dataAttack of data.startedAttacks[key])
{
let attack = new m.AttackPlan(gameState, this.Config, 0);
attack.Deserialize(gameState, dataAttack);
attack.init(gameState);
this.startedAttacks[key].push(attack);
}
};
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 16532)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 16533)
@@ -1,1785 +1,1800 @@
var PETRA = function(m)
{
/* This is an attack plan:
* It deals with everything in an attack, from picking a target to picking a path to it
* To making sure units are built, and pushing elements to the queue manager otherwise
* It also handles the actual attack, though much work is needed on that.
*/
m.AttackPlan = function(gameState, Config, uniqueID, type, data)
{
this.Config = Config;
this.name = uniqueID;
this.type = type || "Attack";
this.state = "unexecuted";
if (data && data.target)
{
this.target = data.target;
this.targetPos = this.target.position();
this.targetPlayer = this.target.owner();
}
else
{
this.target = undefined;
this.targetPos = undefined;
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
}
if (this.targetPlayer === undefined)
{
this.failed = true;
return false;
}
// get a starting rallyPoint ... will be improved later
var rallyPoint = undefined;
for (var base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
rallyPoint = base.anchor.position();
break;
}
if (!rallyPoint) // no base ? take the position of any of our entities
{
gameState.getOwnEntities().forEach(function (ent) {
if (rallyPoint || !ent.position())
return;
rallyPoint = ent.position();
});
if (!rallyPoint)
{
this.failed = true;
return false;
}
}
this.rallyPoint = rallyPoint;
this.overseas = undefined;
this.paused = false;
this.maxCompletingTurn = 0;
// priority of the queues we'll create.
var priority = 70;
// priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize".
// if not, this is a "bonus". The higher the priority, the faster this unit will get built.
// Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm)
// Eg: if all are priority 1, and the siege is 0.5, the siege units will get built
// only once every other category is at least 50% of its target size.
// note: siege build order is currently added by the military manager if a fortress is there.
this.unitStat = {};
// neededShips is the minimal number of ships which should be available for transport
if (type === "Rush")
{
priority = 250;
this.unitStat["Infantry"] = { "priority": 1, "minSize": 10, "targetSize": 26, "batchSize": 2, "classes": ["Infantry"],
"interests": [ ["strength",1], ["cost",1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"] ] };
if (data && data.targetSize)
this.unitStat["Infantry"]["targetSize"] = data.targetSize;
this.neededShips = 1;
}
else if (type === "Raid")
{
priority = 150;
this.unitStat["Cavalry"] = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"],
"interests": [ ["strength",1], ["cost",1] ] };
this.neededShips = 1;
}
else if (type === "HugeAttack")
{
priority = 90;
// basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units.
this.unitStat["RangedInfantry"] = { "priority": 0.7, "minSize": 5, "targetSize": 15, "batchSize": 5, "classes": ["Infantry","Ranged", "CitizenSoldier"],
"interests": [["strength",3], ["cost",1] ] };
this.unitStat["MeleeInfantry"] = { "priority": 0.7, "minSize": 5, "targetSize": 15, "batchSize": 5, "classes": ["Infantry","Melee", "CitizenSoldier"],
"interests": [ ["strength",3], ["cost",1] ] };
this.unitStat["ChampRangedInfantry"] = { "priority": 1, "minSize": 5, "targetSize": 25, "batchSize": 5, "classes": ["Infantry","Ranged", "Champion"],
"interests": [["strength",3], ["cost",1] ] };
this.unitStat["ChampMeleeInfantry"] = { "priority": 1, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry","Melee", "Champion"],
"interests": [ ["strength",3], ["cost",1] ] };
this.unitStat["MeleeCavalry"] = { "priority": 0.7, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry","Melee", "CitizenSoldier"],
"interests": [ ["strength",2], ["cost",1] ] };
this.unitStat["RangedCavalry"] = { "priority": 0.7, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry","Ranged", "CitizenSoldier"],
"interests": [ ["strength",2], ["cost",1] ] };
this.unitStat["ChampMeleeInfantry"] = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Melee", "Champion"],
"interests": [ ["strength",3], ["cost",1] ] };
this.unitStat["ChampMeleeCavalry"] = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Cavalry","Melee", "Champion"],
"interests": [ ["strength",2], ["cost",1] ] };
this.unitStat["Hero"] = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"],
"interests": [ ["strength",2], ["cost",1] ] };
this.neededShips = 5;
// change the targetSize according to max population
for (var unitCat in this.unitStat)
this.unitStat[unitCat]["targetSize"] = Math.round(this.Config.popScaling * this.unitStat[unitCat]["targetSize"]);
}
else
{
priority = 70;
this.unitStat["RangedInfantry"] = { "priority": 1, "minSize": 6, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Ranged"],
"interests": [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] };
this.unitStat["MeleeInfantry"] = { "priority": 1, "minSize": 6, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Melee"],
"interests": [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] };
this.neededShips = 3;
}
// Put some randomness on the attack size
var variation = 0.8 + 0.4*Math.random();
// and lower priority and smaller sizes for easier difficulty levels
if (this.Config.difficulty < 2)
{
priority *= 0.6;
variation *= 0.6;
}
else if (this.Config.difficulty < 3)
{
priority *= 0.8;
variation *= 0.8;
}
for (var cat in this.unitStat)
{
this.unitStat[cat]["targetSize"] = Math.round(variation * this.unitStat[cat]["targetSize"]);
this.unitStat[cat]["minSize"] = Math.min(this.unitStat[cat]["minSize"], this.unitStat[cat]["targetSize"]);
}
// TODO: there should probably be one queue per type of training building
gameState.ai.queueManager.addQueue("plan_" + this.name, priority);
gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1);
gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority);
// each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ]
this.buildOrder = [];
this.canBuildUnits = gameState.ai.HQ.canBuildUnits;
// some variables for during the attack
this.position5TurnsAgo = [0,0];
this.lastPosition = [0,0];
this.position = [0,0];
// get a good path to an estimated target.
this.pathFinder = new API3.aStarPath(gameState, false, false, this.targetPlayer);
//Engine.DumpImage("widthmap.png", this.pathFinder.widthMap, this.pathFinder.width,this.pathFinder.height,255);
this.pathWidth = 6; // prefer a path far from entities. This will avoid units getting stuck in trees and also results in less straight paths.
this.pathSampling = 2;
return true;
};
m.AttackPlan.prototype.init = function(gameState)
{
this.queue = gameState.ai.queues["plan_" + this.name];
this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"];
this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"];
this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name));
this.unitCollection.registerUpdates();
this.unit = {};
// defining the entity collections. Will look for units I own, that are part of this plan.
// Also defining the buildOrders.
for (var cat in this.unitStat)
{
var Unit = this.unitStat[cat];
this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit["classes"]));
this.unit[cat].registerUpdates();
if (this.canBuildUnits)
this.buildOrder.push([0, Unit["classes"], this.unit[cat], Unit, cat]);
}
};
m.AttackPlan.prototype.getName = function()
{
return this.name;
};
m.AttackPlan.prototype.getType = function()
{
return this.type;
};
m.AttackPlan.prototype.isStarted = function()
{
return (this.state !== "unexecuted" && this.state !== "completing");
};
m.AttackPlan.prototype.isPaused = function()
{
return this.paused;
};
m.AttackPlan.prototype.setPaused = function(boolValue)
{
this.paused = boolValue;
};
// Returns true if the attack can be executed at the current time
// Basically it checks we have enough units.
m.AttackPlan.prototype.canStart = function(gameState)
{
if (!this.canBuildUnits)
return true;
for (var unitCat in this.unitStat)
{
var Unit = this.unitStat[unitCat];
if (this.unit[unitCat].length < Unit["minSize"])
return false;
}
return true;
};
m.AttackPlan.prototype.mustStart = function(gameState)
{
if (this.isPaused() || this.path === undefined)
return false;
if (!this.canBuildUnits)
return true;
var MaxReachedEverywhere = true;
var MinReachedEverywhere = true;
for (var unitCat in this.unitStat)
{
var Unit = this.unitStat[unitCat];
if (this.unit[unitCat].length < Unit["targetSize"])
MaxReachedEverywhere = false;
if (this.unit[unitCat].length < Unit["minSize"])
{
MinReachedEverywhere = false;
break;
}
}
if (MaxReachedEverywhere)
return true;
if (MinReachedEverywhere)
{
if ((gameState.getPopulationMax() - gameState.getPopulation() < 10) ||
(this.type === "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 60))
return true;
}
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)
{
if (!this.isStarted())
{
// no minsize as we don't want the plan to fail at the last minute though.
this.unitStat[name] = unitStats;
var Unit = this.unitStat[name];
var filter = API3.Filters.and(API3.Filters.byClassesAnd(Unit["classes"]), API3.Filters.byMetadata(PlayerID, "plan", this.name));
this.unit[name] = gameState.getOwnUnits().filter(filter);
this.unit[name].registerUpdates();
this.buildOrder.push([0, Unit["classes"], this.unit[name], Unit, name]);
if (resetQueue)
{
this.queue.empty();
this.queueChamp.empty();
this.queueSiege.empty();
}
}
};
m.AttackPlan.prototype.addSiegeUnits = function(gameState)
{
if (this.unitStat["Siege"] || this.state !== "unexecuted")
return false;
// no minsize as we don't want the plan to fail at the last minute though.
var stat = { "priority": 1., "minSize": 0, "targetSize": 4, "batchSize": 2, "classes": ["Siege"],
"interests": [ ["siegeStrength", 3], ["cost",1] ] };
if (gameState.civ() === "maur")
stat["classes"] = ["Elephant", "Champion"];
if (this.Config.difficulty < 2)
stat["targetSize"] = 1;
else if (this.Config.difficulty < 3)
stat["targetSize"] = 2;
this.addBuildOrder(gameState, "Siege", stat, true);
return true;
};
// Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start"
// 3 is a special case: no valid path returned. Right now I stop attacking alltogether.
m.AttackPlan.prototype.updatePreparation = function(gameState)
{
// the completing step is used to return resources and regroup the units
// so we check that we have no more forced order before starting the attack
if (this.state === "completing")
{
// check that all units have finished with their transport if needed
if (this.waitingForTransport())
return 1;
// bloqued units which cannot finish their order should not stop the attack
if (gameState.ai.playedTurn < this.maxCompletingTurn && this.hasForceOrder())
return 1;
return 2;
}
if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 == 0)
this.debugAttack();
// find our target (if not yet done or because our previous one was destroyed)
if (!this.target || !gameState.getEntityById(this.target.id()))
{
this.target = this.getNearestTarget(gameState, this.rallyPoint);
if (!this.target)
{
// may-be all our previous enemey targets have been destroyed ?
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer !== undefined)
this.target = this.getNearestTarget(gameState, this.rallyPoint);
if (!this.target)
return 0;
}
this.targetPos = this.target.position();
// redefine a new rally point for this target if we have a base on the same land
// find a new one on the pseudo-nearest base (dist weighted by the size of the island)
var targetIndex = gameState.ai.accessibility.getAccessValue(this.targetPos);
var rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
if (targetIndex !== rallyIndex)
{
var distminSame = Math.min();
var rallySame = undefined;
var distminDiff = Math.min();
var rallyDiff = undefined;
for (var base of gameState.ai.HQ.baseManagers)
{
var anchor = base.anchor;
if (!anchor || !anchor.position())
continue;
var dist = API3.SquareVectorDistance(anchor.position(), this.targetPos);
if (base.accessIndex === targetIndex)
{
if (dist < distminSame)
{
distminSame = dist;
rallySame = anchor.position();
}
}
else
{
dist = dist / Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]);
if (dist < distminDiff)
{
distminDiff = dist;
rallyDiff = anchor.position();
}
}
}
if (rallySame)
this.rallyPoint = rallySame;
else if (rallyDiff)
{
this.rallyPoint = rallyDiff;
this.overseas = gameState.ai.HQ.getSeaIndex(gameState, rallyIndex, targetIndex);
if (this.overseas)
gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips);
else
return 0;
}
}
// reset the path so that we recompute it for this new target
this.path = undefined;
if (!this.pathFinder)
{
this.pathFinder = new API3.aStarPath(gameState, false, false, this.targetPlayer);
this.pathWidth = 6;
this.pathSampling = 2;
}
}
// when we have a target, we path to it.
// I'd like a good high width sampling first.
// Thus I will not do everything at once.
// It will probably carry over a few turns but that's no issue.
if (this.path === undefined || this.path === "toBeContinued")
{
var ret = this.getPathToTarget(gameState);
if (ret >= 0)
return ret;
}
// if we need a transport, wait for some transport ships
if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length)
return 1;
this.assignUnits(gameState);
if (this.type !== "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined)
this.reassignCavUnit(gameState); // reassign some cav (if any) to fasten raid preparations
// special case: if we've reached max pop, and we can start the plan, start it.
if (gameState.getPopulationMax() - gameState.getPopulation() < 10)
{
if (this.canStart())
{
this.queue.empty();
this.queueChamp.empty();
this.queueSiege.empty();
}
else // Abort the plan so that its units will be reassigned to other plans.
{
if (this.Config.debug > 1)
{
var am = gameState.ai.HQ.attackManager;
API3.warn(" attacks upcoming: raid " + am.upcomingAttacks["Raid"].length
+ " rush " + am.upcomingAttacks["Rush"].length
+ " attack " + am.upcomingAttacks["Attack"].length
+ " huge " + am.upcomingAttacks["HugeAttack"].length);
API3.warn(" attacks started: raid " + am.startedAttacks["Raid"].length
+ " rush " + am.startedAttacks["Rush"].length
+ " attack " + am.startedAttacks["Attack"].length
+ " huge " + am.startedAttacks["HugeAttack"].length);
}
return 0;
}
}
else if (this.mustStart(gameState) && (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0))
{
// keep on while the units finish being trained, then we'll start
this.queue.empty();
this.queueChamp.empty();
this.queueSiege.empty();
return 1;
}
else if (!this.mustStart(gameState))
{
if (this.canBuildUnits)
{
// We still have time left to recruit units and do stuffs.
this.trainMoreUnits(gameState);
// may happen if we have no more training facilities and build orders are canceled
if (this.buildOrder.length == 0)
return 0; // will abort the plan
}
return 1;
}
// if we're here, it means we must start (and have no units in training left).
this.state = "completing";
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);
for (var entity of this.unitCollection.values())
{
// For the time being, if occupied in a transport, remove the unit from this plan TODO improve that
if (entity.getMetadata(PlayerID, "transport") !== undefined || entity.getMetadata(PlayerID, "transporter") !== undefined)
{
entity.setMetadata(PlayerID, "plan", -1);
continue;
}
entity.setMetadata(PlayerID, "role", "attack");
entity.setMetadata(PlayerID, "subrole", "completing");
var queued = false;
if (entity.resourceCarrying() && entity.resourceCarrying().length)
queued = m.returnResources(entity, gameState);
var index = gameState.ai.accessibility.getAccessValue(entity.position());
if (index === rallyIndex)
entity.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued);
else
gameState.ai.HQ.navalManager.requireTransport(gameState, entity, index, rallyIndex, rallyPoint);
}
// reset all queued units
var plan = this.name;
gameState.ai.queueManager.removeQueue("plan_" + plan);
gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ");
gameState.ai.queueManager.removeQueue("plan_" + plan + "_siege");
return 1;
};
m.AttackPlan.prototype.trainMoreUnits = function(gameState)
{
// let's sort by training advancement, ie 'current size / target size'
// count the number of queued units too.
// substract priority.
for (var i = 0; i < this.buildOrder.length; ++i)
{
var special = "Plan_" + this.name + "_" + this.buildOrder[i][4];
var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special);
aQueued += this.queue.countQueuedUnitsWithMetadata("special", special);
aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special);
aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special);
this.buildOrder[i][0] = this.buildOrder[i][2].length + aQueued;
}
this.buildOrder.sort(function (a,b) {
var va = a[0]/a[3]["targetSize"] - a[3]["priority"];
if (a[0] >= a[3]["targetSize"])
va += 1000;
var vb = b[0]/b[3]["targetSize"] - b[3]["priority"];
if (b[0] >= b[3]["targetSize"])
vb += 1000;
return va - vb;
});
if (this.Config.debug > 1 && gameState.ai.playedTurn%50 == 0)
{
API3.warn("====================================");
API3.warn("======== build order for plan " + this.name);
for (var order of this.buildOrder)
{
var specialData = "Plan_"+this.name+"_"+order[4];
var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData);
var queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData);
var queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData);
var queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData);
API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining
+ " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize);
}
API3.warn("====================================");
}
if (this.buildOrder[0][0] < this.buildOrder[0][3]["targetSize"])
{
// find the actual queue we want
var queue = this.queue;
if (this.buildOrder[0][3]["classes"].indexOf("Siege") !== -1 ||
(gameState.civ() == "maur" && this.buildOrder[0][3]["classes"].indexOf("Elephant") !== -1 && this.buildOrder[0][3]["classes"].indexOf("Champion")))
queue = this.queueSiege;
else if (this.buildOrder[0][3]["classes"].indexOf("Hero") !== -1)
queue = this.queueSiege;
else if (this.buildOrder[0][3]["classes"].indexOf("Champion") !== -1)
queue = this.queueChamp;
if (queue.length() <= 5)
{
var template = gameState.ai.HQ.findBestTrainableUnit(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"]);
// HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder,
// effectively removing the unit from the plan.
if (template === undefined)
{
if (this.Config.debug > 1)
API3.warn("attack no template found " + this.buildOrder[0][1]);
delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat.
this.buildOrder.splice(0,1);
}
else
{
if (this.Config.debug > 2)
API3.warn("attack template " + template + " added for plan " + this.name);
var max = this.buildOrder[0][3]["batchSize"];
var specialData = "Plan_" + this.name + "_" + this.buildOrder[0][4];
if (gameState.getTemplate(template).hasClass("CitizenSoldier"))
var trainingPlan = new m.TrainingPlan(gameState, template, { "role": "worker", "plan": this.name, "special": specialData, "base": 0 }, max, max);
else
var trainingPlan = new m.TrainingPlan(gameState, template, { "role": "attack", "plan": this.name, "special": specialData, "base": 0 }, max, max);
if (trainingPlan.template)
queue.addItem(trainingPlan);
else if (this.Config.debug > 1)
API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(this.buildOrder[0][1])
+ " build3 " + uneval(this.buildOrder[0][3]["interests"]));
}
}
}
};
m.AttackPlan.prototype.assignUnits = function(gameState)
{
var plan = this.name;
var added = false;
// If we can not build units, assign all available except those affected to allied defense to the current attack
if (!this.canBuildUnits)
{
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.position())
continue;
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1)
continue;
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
continue;
if (ent.getMetadata(PlayerID, "allied"))
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
}
if (this.type === "Raid")
{
// Raid are fast cavalry attack: assign all cav except some for hunting
var num = 0;
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.hasClass("Cavalry"))
continue;
if (!ent.position())
continue;
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1)
continue;
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
continue;
if (num++ < 2)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
}
// Assign all units without specific role
for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values())
{
if (!ent.hasClass("Unit"))
continue;
if (!ent.position())
continue;
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1)
continue;
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
continue;
if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
// Add units previously in a plan, but which left it because needed for defense or attack finished
for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values())
{
if (!ent.position())
continue;
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
// Finally add also some workers,
// If Rush, assign all kind of workers, keeping a minimum number of defenders
// Otherwise, assign only idle workers if too much of them
let worker = gameState.getOwnEntitiesByRole("worker", true);
let num = 0;
let numbase = {};
for (let ent of worker.values())
{
if (!ent.position())
continue;
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1)
continue;
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
if (!ent.hasClass("CitizenSoldier"))
continue;
let baseID = ent.getMetadata(PlayerID, "base");
if (baseID)
numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1;
else
{
API3.warn("Petra problem ent without base ");
m.dumpEntity(ent);
continue;
}
if (this.type !== "Rush" && ent.getMetadata(PlayerID, "subrole") !== "idle")
continue;
if (num++ < 9 || numbase[baseID] < 5)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
};
// Reassign one (at each turn) Cav unit to fasten raid preparation
m.AttackPlan.prototype.reassignCavUnit = function(gameState)
{
var found = undefined;
for (var ent of this.unitCollection.values())
{
if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier"))
continue;
found = ent;
break;
}
if (!found)
return;
let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid");
found.setMetadata(PlayerID, "plan", raid.name);
this.unitCollection.updateEnt(found);
raid.unitCollection.updateEnt(found);
};
// sameLand true means that we look for a target for which we do not need to take a transport
m.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand)
{
if (this.type === "Raid")
var targets = this.raidTargetFinder(gameState);
else if (this.type === "Rush" || this.type === "Attack")
var targets = this.rushTargetFinder(gameState, this.targetPlayer);
else
var targets = this.defaultTargetFinder(gameState, this.targetPlayer);
if (targets.length == 0)
return undefined;
var land = gameState.ai.accessibility.getAccessValue(position);
// picking the nearest target
var minDist = -1;
var target = undefined;
for (var ent of targets.values())
{
if (!ent.position())
continue;
if (sameLand && gameState.ai.accessibility.getAccessValue(ent.position()) != land)
continue;
var dist = API3.SquareVectorDistance(ent.position(), position);
if (dist < minDist || minDist == -1)
{
minDist = dist;
target = ent;
}
}
if (!target)
return undefined;
// Rushes can change their enemy target if nothing found with the preferred enemy
this.targetPlayer = target.owner();
return target;
};
// Default target finder aims for conquest critical targets
m.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy)
{
var targets = undefined;
if (gameState.getGameType() === "wonder")
{
targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder"));
if (targets.length)
return targets;
}
var targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("CivCentre"));
if (!targets.length)
targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("ConquestCritical"));
// If there's nothing, attack anything else that's less critical
if (!targets.length)
targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Town"));
if (!targets.length)
targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Village"));
// no buildings, attack anything conquest critical, even units
if (!targets.length)
targets = gameState.getEnemyEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical"));
return targets;
};
// Rush target finder aims at isolated non-defended buildings
m.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy)
{
var targets = new API3.EntityCollection(gameState.sharedScript);
if (playerEnemy !== undefined)
var buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray();
else
var buildings = gameState.getEnemyStructures().toEntityArray();
if (buildings.length == 0)
return targets;
this.position = this.unitCollection.getCentrePosition();
if (!this.position)
this.position = this.rallyPoint;
var minDist = Math.min();
var target = undefined;
for (var building of buildings)
{
if (building.owner() == 0)
continue;
if (building.getDefaultArrow() || building.getArrowMultiplier())
continue;
var pos = building.position();
var defended = false;
for (var defense of buildings)
{
if (!building.getDefaultArrow() && !building.getArrowMultiplier())
continue;
var dist = API3.SquareVectorDistance(pos, defense.position());
if (dist < 6400) // TODO check on defense range rather than this fixed 80*80
{
defended = true;
break;
}
}
if (defended)
continue;
var dist = API3.SquareVectorDistance(pos, this.position);
if (dist > minDist)
continue;
minDist = dist;
target = building;
}
if (target)
targets.addEnt(target);
if (targets.length == 0)
{
if (this.type === "Attack")
targets = this.defaultTargetFinder(gameState, playerEnemy);
else if (this.type === "Rush" && playerEnemy)
targets = this.rushTargetFinder(gameState);
}
return targets;
};
// Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders
m.AttackPlan.prototype.raidTargetFinder = function(gameState)
{
var targets = new API3.EntityCollection(gameState.sharedScript);
for (var targetId of gameState.ai.HQ.defenseManager.targetList)
{
var target = gameState.getEntityById(targetId);
if (target && target.position())
targets.addEnt(target);
}
return targets
};
m.AttackPlan.prototype.getPathToTarget = function(gameState)
{
if (this.path === undefined)
this.path = this.pathFinder.getPath(this.rallyPoint, this.targetPos, this.pathSampling, this.pathWidth, 175);
else if (this.path === "toBeContinued")
this.path = this.pathFinder.continuePath();
if (this.path === undefined)
{
if (this.pathWidth == 6)
{
this.pathWidth = 2;
delete this.path;
}
else
{
delete this.pathFinder;
return 3; // no path.
}
}
else if (this.path === "toBeContinued")
{
return 1; // carry on
}
else if (this.path[1] === true && this.pathWidth == 2)
{
// okay so we need a ship.
// Basically we'll add it as a new class to train compulsorily, and we'll recompute our path.
if (!gameState.ai.HQ.navalMap)
{
gameState.ai.HQ.navalMap = true;
return 0;
}
this.pathWidth = 3;
this.pathSampling = 3;
this.path = this.path[0].reverse();
delete this.pathFinder;
// Change the rally point to something useful (should avoid rams getting stuck in our territor)
this.setRallyPoint(gameState);
}
else if (this.path[1] === true && this.pathWidth == 6)
{
// retry with a smaller pathwidth:
this.pathWidth = 2;
delete this.path;
}
else
{
this.path = this.path[0].reverse();
delete this.pathFinder;
// Change the rally point to something useful (should avoid rams getting stuck in our territor)
this.setRallyPoint(gameState);
}
return -1; // ok
};
m.AttackPlan.prototype.setRallyPoint = function(gameState)
{
for (var i = 0; i < this.path.length; ++i)
{
// my pathfinder returns arrays in arrays in arrays.
var waypointPos = this.path[i][0];
if (gameState.ai.HQ.territoryMap.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true)
{
// Set rally point at the border of our territory
// or where we need to change transportation method.
if (i !== 0)
this.rallyPoint = this.path[i-1][0];
else
this.rallyPoint = this.path[0][0];
if (i >= 2)
this.path.splice(0, i-1);
break;
}
}
};
// Executes the attack plan, after this is executed the update function will be run every turn
// If we're here, it's because we have enough units.
m.AttackPlan.prototype.StartAttack = function(gameState)
{
if (this.Config.debug > 1)
API3.warn("start attack " + this.name + " with type " + this.type);
if (!this.target || !gameState.getEntityById(this.target.id())) // our target was destroyed during our preparation
{
if (!this.targetPos) // should not happen
return false;
var targetIndex = gameState.ai.accessibility.getAccessValue(this.targetPos);
var rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
if (targetIndex === rallyIndex)
{
// If on the same index: if we are doing a raid, look for a better target,
// otherwise proceed with the previous target position
// and we will look for a better target there
if (this.type === "Raid")
{
this.target = this.getNearestTarget(gameState, this.rallyPoint);
if (!this.target)
return false;
this.targetPos = this.target.position();
}
}
else
{
// Not on the same index, do not loose time to go to previous targetPos if nothing there
// so directly look for a new target right now
this.target = this.getNearestTarget(gameState, this.rallyPoint);
if (!this.target)
return false;
this.targetPos = this.target.position();
}
}
// check we have a target and a path.
if (this.targetPos && this.path !== undefined)
{
// erase our queue. This will stop any leftover unit from being trained.
gameState.ai.queueManager.removeQueue("plan_" + this.name);
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ");
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege");
var curPos = this.unitCollection.getCentrePosition();
for (var ent of this.unitCollection.values())
ent.setMetadata(PlayerID, "subrole", "walking");
this.unitCollection.setStance("aggressive");
if (gameState.ai.accessibility.getAccessValue(this.targetPos) === gameState.ai.accessibility.getAccessValue(this.rallyPoint))
{
if (!this.path[0][0][0] || !this.path[0][0][1])
{
if (this.Config.debug > 1)
API3.warn("StartAttack: Problem with path " + uneval(this.path));
return false;
}
this.state = "walking";
this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]);
}
else
{
this.state = "transporting";
var startIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
var endIndex = gameState.ai.accessibility.getAccessValue(this.targetPos);
var endPos = this.targetPos;
// TODO require a global transport for the collection,
// and put back its state to "walking" when the transport is finished
for (var ent of this.unitCollection.values())
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, startIndex, endIndex, endPos);
}
}
else
{
gameState.ai.gameFinished = true;
API3.warn("I do not have any target. So I'll just assume I won the game.");
return false;
}
return true;
};
// Runs every turn after the attack is executed
m.AttackPlan.prototype.update = function(gameState, events)
{
if (this.unitCollection.length == 0)
return 0;
Engine.ProfileStart("Update Attack");
this.position = this.unitCollection.getCentrePosition();
var IDs = this.unitCollection.toIdArray();
var self = this;
// we are transporting our units, let's wait
// TODO instead of state "arrived", made a state "walking" with a new path
if (this.state === "transporting")
{
var done = true;
for (var ent of this.unitCollection.values())
{
if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined)
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]});
else if (this.Config.debug > 1)
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [1,1,1]});
if (!done)
continue;
if (ent.getMetadata(PlayerID, "transport") !== undefined)
done = false;
}
if (done)
this.state = "arrived";
else
{
// if we are attacked while waiting the rest of the army, retaliate
var attackedEvents = events["Attacked"];
for (var evt of attackedEvents)
{
if (IDs.indexOf(evt.target) == -1)
continue;
var attacker = gameState.getEntityById(evt.attacker);
var ourUnit = gameState.getEntityById(evt.target);
if (!attacker || !ourUnit)
continue;
for (var ent of this.unitCollection.values())
{
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
if (!ent.isIdle())
continue;
ent.attack(attacker.id());
}
break;
}
}
}
// this actually doesn't do anything right now.
if (this.state === "walking")
{
// we're marching towards the target
// Let's check if any of our unit has been attacked.
// In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building
// or if we reached the enemy base. Different plans may react differently.
var attackedNB = 0;
var attackedUnitNB = 0;
var attackedEvents = events["Attacked"];
for (var evt of attackedEvents)
{
if (IDs.indexOf(evt.target) == -1)
continue;
var attacker = gameState.getEntityById(evt.attacker);
var ourUnit = gameState.getEntityById(evt.target);
if (attacker && (attacker.owner() != 0 || this.targetPlayer == 0))
{
attackedNB++;
if (attacker.hasClass("Unit"))
attackedUnitNB++;
}
}
// Are we arrived at destination ?
var maybe = true;
if (attackedUnitNB == 0)
{
var siegeNB = 0;
for (var ent of this.unitCollection.values())
if (this.isSiegeUnit(gameState, ent))
siegeNB++;
if (siegeNB == 0)
maybe = false;
}
if (maybe && ((gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer && attackedNB > 1) || attackedNB > 3))
this.state = "arrived";
}
if (this.state === "walking")
{
// basically haven't moved an inch: very likely stuck)
if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 == 0)
{
// check for stuck siege units
var farthest = 0;
var farthestEnt = -1;
this.unitCollection.filter(API3.Filters.byClass("Siege")).forEach (function (ent) {
var dist = API3.SquareVectorDistance(ent.position(), self.position);
if (dist < farthest)
return;
farthest = dist;
farthestEnt = ent;
});
if (farthestEnt !== -1)
farthestEnt.destroy();
}
if (gameState.ai.playedTurn % 5 == 0)
this.position5TurnsAgo = this.position;
if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0)
{
if (!this.path[0][0][0] || !this.path[0][0][1])
API3.warn("Start: Problem with path " + uneval(this.path));
// We're stuck, presumably. Check if there are no walls just close to us. If so, we're arrived, and we're gonna tear down some serious stone.
var nexttoWalls = false;
gameState.getEnemyStructures().filter(API3.Filters.byClass("StoneWall")).forEach( function (ent) {
if (!nexttoWalls && API3.SquareVectorDistance(self.position, ent.position()) < 800)
nexttoWalls = true;
});
// there are walls but we can attack
if (nexttoWalls && this.unitCollection.filter(API3.Filters.byCanAttack("StoneWall")).length !== 0)
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy.");
this.state = "arrived";
}
else if (nexttoWalls) // abort plan
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up.");
Engine.ProfileStop();
return 0;
}
else
//this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]);
this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]);
}
}
// check if our units are close enough from the next waypoint.
if (this.state === "walking")
{
if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000)
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
this.state = "arrived";
}
else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0][0]) < 1600)
{
this.path.shift();
if (this.path.length)
this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]);
else
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
this.state = "arrived";
}
}
}
if (this.state === "arrived")
{
// let's proceed on with whatever happens now.
this.state = "";
this.startingAttack = true;
this.unitCollection.forEach( function (ent) {
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "attacking");
});
if (this.type === "Rush") // try to find a better target for rush
{
var newtarget = this.getNearestTarget(gameState, this.position);
if (newtarget)
{
this.target = newtarget;
this.targetPos = this.target.position();
}
}
}
// basic state of attacking.
if (this.state === "")
{
// First update the target if needed:
if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned
this.target = undefined;
if (!this.target || !gameState.getEntityById(this.target.id()))
{
if (this.Config.debug > 1)
API3.warn("Seems like our target has been destroyed. Switching.");
this.target = this.getNearestTarget(gameState, this.position, true);
if (!this.target)
{
// Check if we could help any current attack
var attackManager = gameState.ai.HQ.attackManager;
var accessIndex = gameState.ai.accessibility.getAccessValue(this.position);
for (let attackType in attackManager.startedAttacks)
{
if (this.target)
break;
for (let attack of attackManager.startedAttacks[attackType])
{
if (attack.name === this.name)
continue;
if (!attack.target || !gameState.getEntityById(attack.target.id()))
continue;
if (accessIndex !== gameState.ai.accessibility.getAccessValue(attack.targetPos))
continue;
if (attack.target.owner() === 0 && attack.targetPlayer !== 0) // looks like it has resigned
continue;
this.target = attack.target;
this.targetPlayer = attack.targetPlayer;
break;
}
}
// If not, let's look for another enemy
if (!this.target)
{
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer !== undefined)
this.target = this.getNearestTarget(gameState, this.position, true);
if (!this.target)
{
if (this.Config.debug > 1)
API3.warn("No new target found. Remaining units " + this.unitCollection.length);
Engine.ProfileStop();
return false;
}
}
if (this.Config.debug > 1)
API3.warn("We will help one of our other attacks");
}
this.targetPos = this.target.position();
}
// and regularly update the target position in case it's a unit.
if (this.target.hasClass("Unit"))
this.targetPos = this.target.position();
var time = gameState.ai.elapsedTime;
var attackedEvents = events["Attacked"];
for (var evt of attackedEvents)
{
if (IDs.indexOf(evt.target) == -1)
continue;
var attacker = gameState.getEntityById(evt.attacker);
if (!attacker || !attacker.position() || !attacker.hasClass("Unit"))
continue;
var ourUnit = gameState.getEntityById(evt.target);
if (this.isSiegeUnit(gameState, ourUnit))
{ // if our siege units are attacked, we'll send some units to deal with enemies.
var collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5);
for (var ent of collec.values())
{
if (this.isSiegeUnit(gameState, ent)) // needed as mauryan elephants are not filtered out
continue;
ent.attack(attacker.id());
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
// And if this attacker is a non-ranged siege unit and our unit also, attack it
if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee"))
{
ourUnit.attack(attacker.id());
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
else
{
if (this.isSiegeUnit(gameState, attacker))
{ // if our unit is attacked by a siege unit, we'll send some melee units to help it.
var collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
for (var ent of collec.values())
{
ent.attack(attacker.id());
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
else
{ // if units are attacked, abandon their target (if it was a structure or a support) and retaliate
// also if our unit is attacking a range unit and the attacker is a melee unit, retaliate
var orderData = ourUnit.unitAIOrderData();
if (orderData.length !== 0 && orderData[0]["target"])
{
var target = gameState.getEntityById(orderData[0]["target"]);
if (target && !target.hasClass("Structure") && !target.hasClass("Support"))
{
if (!target.hasClass("Ranged") || !attacker.hasClass("Melee"))
continue;
}
}
ourUnit.attack(attacker.id());
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
}
var enemyUnits = gameState.getEnemyUnits(this.targetPlayer);
var enemyStructures = gameState.getEnemyStructures(this.targetPlayer);
// Count the number of times an enemy is targeted, to prevent all units to follow the same target
let unitTargets = {};
for (let ent of this.unitCollection.values())
{
if (ent.hasClass("Ship")) // TODO What to do with ships
continue;
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0]["target"])
continue;
let targetId = orderData[0]["target"];
let target = gameState.getEntityById(targetId);
if (!target || target.hasClass("Structure"))
continue;
if (!(targetId in unitTargets))
{
if (this.isSiegeUnit(gameState, target) || target.hasClass("Hero"))
unitTargets[targetId] = -8;
else if (target.hasClass("Champion") || target.hasClass("Ship"))
unitTargets[targetId] = -5;
else
unitTargets[targetId] = -3;
}
++unitTargets[targetId];
}
let veto = {};
for (let target in unitTargets)
if (unitTargets[target] > 0)
veto[target] = true;
var targetClassesUnit;
var targetClassesSiege;
if (this.type === "Rush")
targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["StoneWall", "Tower", "Fortress"], "vetoEntities": veto};
else
{
if (this.target.hasClass("Fortress"))
targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["StoneWall"], "vetoEntities": veto};
else if (this.target.hasClass("StoneWall"))
targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto};
else
targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Fortress", "StoneWall"], "vetoEntities": veto};
}
if (this.target.hasClass("Structure"))
targetClassesSiege = {"attack": ["Structure"], "vetoEntities": veto};
else
targetClassesSiege = {"attack": ["Unit", "Structure"], "vetoEntities": veto};
if (this.unitCollUpdateArray === undefined || this.unitCollUpdateArray.length == 0)
this.unitCollUpdateArray = this.unitCollection.toIdArray();
// Let's check a few units each time we update (currently 10) except when attack starts
if (this.unitCollUpdateArray.length < 15 || this.startingAttack)
var lgth = this.unitCollUpdateArray.length;
else
var lgth = 10;
for (var check = 0; check < lgth; check++)
{
var ent = gameState.getEntityById(this.unitCollUpdateArray[check]);
if (!ent || !ent.position())
continue;
let targetId = undefined;
let orderData = ent.unitAIOrderData();
if (orderData && orderData.length && orderData[0]["target"])
targetId = orderData[0]["target"];
// update the order if needed
var needsUpdate = false;
var maybeUpdate = false;
var siegeUnit = this.isSiegeUnit(gameState, ent);
if (ent.isIdle())
needsUpdate = true;
else if (siegeUnit && targetId)
{
var target = gameState.getEntityById(targetId);
if (!target)
needsUpdate = true;
else if (unitTargets[targetId] && unitTargets[targetId] > 0)
{
needsUpdate = true;
--unitTargets[targetId];
}
else if (!target.hasClass("Structure"))
maybeUpdate = true;
}
else if (targetId)
{
var target = gameState.getEntityById(targetId);
if (!target)
needsUpdate = true;
else if (unitTargets[targetId] && unitTargets[targetId] > 0)
{
needsUpdate = true;
--unitTargets[targetId];
}
else if (target.hasClass("Structure") || (target.hasClass("Ship") && !ent.hasClass("Ship")))
maybeUpdate = true;
else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged")
&& target.hasClass("Female") && target.unitAIState().split(".")[1] == "FLEEING")
maybeUpdate = true;
}
// don't update too soon if not necessary
if (!needsUpdate)
{
if (!maybeUpdate)
continue;
let deltat = (ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") ? 10 : 5;
var lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime");
if (lastAttackPlanUpdateTime && (time - lastAttackPlanUpdateTime) < deltat)
continue;
}
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
var range = 60;
var attackTypes = ent.attackTypes();
if (attackTypes && attackTypes.indexOf("Ranged") !== -1)
range = 30 + ent.attackRange("Ranged").max;
else if (ent.hasClass("Cavalry"))
range += 30;
range = range * range;
var entIndex = gameState.ai.accessibility.getAccessValue(ent.position());
// Checking for gates if we're a siege unit.
if (siegeUnit)
{
var mStruct = enemyStructures.filter(function (enemy) {
if (!enemy.position() || (enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
if (enemy.foundationProgress() == 0)
return false;
if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex)
return false;
return true;
}).toEntityArray();
if (mStruct.length !== 0)
{
mStruct.sort(function (structa,structb)
{
var vala = structa.costSum();
if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall"))
vala += 10000;
else if (structa.getDefaultArrow() || structa.getArrowMultiplier())
vala += 1000;
else if (structa.hasClass("ConquestCritical"))
vala += 200;
var valb = structb.costSum();
if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall"))
valb += 10000;
else if (structb.getDefaultArrow() || structb.getArrowMultiplier())
valb += 1000;
else if (structb.hasClass("ConquestCritical"))
valb += 200;
return (valb - vala);
});
if (mStruct[0].hasClass("Gates"))
ent.attack(mStruct[0].id());
else
{
var rand = Math.floor(Math.random() * mStruct.length * 0.2);
ent.attack(mStruct[rand].id());
}
}
else
{
if (!ent.hasClass("Ranged"))
{
let targetClasses = {"attack": targetClassesSiege.attack, "avoid": ["Ship"], "vetoEntities": veto};
- ent.attackMove(self.targetPos[0], self.targetPos[1], targetClasses);
+ ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
}
else
- ent.attackMove(self.targetPos[0], self.targetPos[1], targetClassesSiege);
+ ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege);
}
}
else
{
var nearby = (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged"));
var mUnit = enemyUnits.filter(function (enemy) {
if (!enemy.position())
return false;
if (enemy.hasClass("Animal"))
return false;
if (nearby && enemy.hasClass("Female") && enemy.unitAIState().split(".")[1] == "FLEEING")
return false;
var dist = API3.SquareVectorDistance(enemy.position(), ent.position());
if (dist > range)
return false;
if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex)
return false;
// if already too much units targeting this enemy, let's continue towards our main target
if (veto[enemy.id()] && API3.SquareVectorDistance(self.targetPos, ent.position()) > 2500)
return false;
enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist));
return true;
}).toEntityArray();
if (mUnit.length !== 0)
{
mUnit.sort(function (unitA,unitB) {
var vala = unitA.hasClass("Support") ? 50 : 0;
if (ent.countersClasses(unitA.classes()))
vala += 100;
var valb = unitB.hasClass("Support") ? 50 : 0;
if (ent.countersClasses(unitB.classes()))
valb += 100;
var distA = unitA.getMetadata(PlayerID, "distance");
var distB = unitB.getMetadata(PlayerID, "distance");
if( distA && distB)
{
vala -= distA;
valb -= distB;
}
if (veto[unitA.id()])
vala -= 20000;
if (veto[unitB.id()])
valb -= 20000;
return valb - vala;
});
var rand = Math.floor(Math.random() * mUnit.length * 0.1);
ent.attack(mUnit[rand].id());
}
- else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 2500 )
+ else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500 )
{
let targetClasses = targetClassesUnit;
if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything
{
if (!ent.hasClass("Ranged") && !ent.hasClass("Ship"))
targetClasses = {"attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto};
else
targetClasses = {"attack": ["Unit", "Structure"], "vetoEntities": veto};
}
else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship"))
targetClasses = {"attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto};
- ent.attackMove(self.targetPos[0], self.targetPos[1], targetClasses);
+ ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
}
else
{
var mStruct = enemyStructures.filter(function (enemy) {
if (!enemy.position() || (enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex)
return false;
return true;
}).toEntityArray();
if (mStruct.length !== 0)
{
mStruct.sort(function (structa,structb) {
var vala = structa.costSum();
if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall"))
vala += 10000;
else if (structa.hasClass("ConquestCritical"))
vala += 100;
var valb = structb.costSum();
if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall"))
valb += 10000;
else if (structb.hasClass("ConquestCritical"))
valb += 100;
return (valb - vala);
});
if (mStruct[0].hasClass("Gates"))
ent.attack(mStruct[0].id());
else
{
var rand = Math.floor(Math.random() * mStruct.length * 0.2);
ent.attack(mStruct[rand].id());
}
}
else if (needsUpdate) // really nothing let's try to help our nearest unit
{
var distmin = Math.min();
var attackerId = undefined;
this.unitCollection.forEach( function (unit) {
if (!unit.position())
return;
if (unit.unitAIState().split(".")[1] !== "COMBAT" || unit.unitAIOrderData().length == 0
|| !unit.unitAIOrderData()[0]["target"])
return;
var dist = API3.SquareVectorDistance(unit.position(), ent.position());
if (dist > distmin)
return;
distmin = dist;
attackerId = unit.unitAIOrderData()[0]["target"];
});
if (attackerId)
ent.attack(attackerId);
}
}
}
}
this.unitCollUpdateArray.splice(0, lgth);
this.startingAttack = false;
// check if this enemy has resigned
if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0)
this.target = undefined;
}
this.lastPosition = this.position;
Engine.ProfileStop();
return this.unitCollection.length;
};
// reset any units
m.AttackPlan.prototype.Abort = function(gameState)
{
this.unitCollection.unregister();
if (this.unitCollection.length)
{
// If the attack was started, and we are on the same land as the rallyPoint, go back there
var rallyPoint = this.rallyPoint;
var withdrawal = (this.isStarted() && !this.overseas);
var self = this;
this.unitCollection.forEach(function(ent) {
ent.stopMoving();
if (withdrawal)
ent.move(rallyPoint[0], rallyPoint[1]);
self.removeUnit(ent);
});
}
for (let unitCat in this.unitStat)
this.unit[unitCat].unregister();
gameState.ai.queueManager.removeQueue("plan_" + this.name);
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ");
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege");
};
m.AttackPlan.prototype.removeUnit = function(ent, update)
{
if (ent.hasClass("CitizenSoldier") && ent.getMetadata(PlayerID, "role") !== "worker")
{
ent.setMetadata(PlayerID, "role", "worker");
ent.setMetadata(PlayerID, "subrole", undefined);
}
ent.setMetadata(PlayerID, "plan", -1);
if (update)
this.unitCollection.updateEnt(ent);
};
m.AttackPlan.prototype.checkEvents = function(gameState, events)
{
let renameEvents = events["EntityRenamed"];
for (let evt of renameEvents)
{
if (this.target && this.target.id() == evt.entity)
{
this.target = gameState.getEntityById(evt.newentity);
if (this.target)
this.targetPos = this.target.position();
}
}
let captureEvents = events["OwnershipChanged"];
for (let evt of captureEvents)
if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to))
this.target = undefined;
if (this.state === "unexecuted")
return;
var TrainingEvents = events["TrainingFinished"];
for (let evt of TrainingEvents)
{
for (let id of evt.entities)
{
let ent = gameState.getEntityById(id);
if (!ent || ent.getMetadata(PlayerID, "plan") === undefined)
continue;
if (ent.getMetadata(PlayerID, "plan") === this.name)
ent.setMetadata(PlayerID, "plan", -1);
}
}
};
m.AttackPlan.prototype.waitingForTransport = function()
{
var waiting = false;
this.unitCollection.forEach(function (ent) {
if (ent.getMetadata(PlayerID, "transport") !== undefined)
waiting = true;
});
return waiting;
};
m.AttackPlan.prototype.hasForceOrder = function(data, value)
{
var forced = false;
this.unitCollection.forEach(function (ent) {
if (data && +(ent.getMetadata(PlayerID, data)) !== value)
return;
var orders = ent.unitAIOrderData();
for (var order of orders)
if (order.force)
forced = true;
});
return forced;
};
m.AttackPlan.prototype.isSiegeUnit = function(gameState, ent)
{
return (ent.hasClass("Siege") || (ent.hasClass("Elephant") && ent.hasClass("Champion")));
};
m.AttackPlan.prototype.debugAttack = function()
{
API3.warn("---------- attack " + this.name);
for (var unitCat in this.unitStat)
{
var Unit = this.unitStat[unitCat];
API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit["minSize"] + " need=" + Unit["targetSize"]);
}
API3.warn("------------------------------");
};
m.AttackPlan.prototype.Serialize = function()
{
let properties = {
"name": this.name,
"type": this.type,
"state": this.state,
"rallyPoint": this.rallyPoint,
"overseas": this.overseas,
"paused": this.paused,
"maxCompletingTurn": this.maxCompletingTurn,
"neededShips": this.neededShips,
"unitStat": this.unitStat,
"position5TurnsAgo": this.position5TurnsAgo,
"lastPosition": this.lastPosition,
"position": this.position,
"targetPlayer": this.targetPlayer,
"target": ((this.target !== undefined) ? this.target.id() : undefined),
"targetPos": this.targetPos
};
let path = {
"path": this.path,
"pathSampling": this.pathSampling,
"pathWidth": this.pathWidth
};
return { "properties": properties, "path": path };
};
m.AttackPlan.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
if (this.target)
this.target = gameState.getEntityById(this.target);
// if the path was not fully computed, we will recompute it as it is not serialized
if (data.path.path != "toBeContinued")
for (let key in data.path)
this[key] = data.path[key];
this.failed = undefined;
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/chatHelper.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/chatHelper.js (revision 16532)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/chatHelper.js (revision 16533)
@@ -1,56 +1,110 @@
var PETRA = function(m)
{
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.");
+ var message = "/team " + markForTranslation("I am launching an attack against %(_player_)s.");
else
- var message = "/team " + markForTranslation("I have just sent an army against %(name)s.");
+ var message = "/team " + markForTranslation("I have just sent an army against %(_player_)s.");
- var chat = { "type": "aichat", "message": message, "translateMessage": true, "translateParameters": ["name"], "parameters": { "name": name } };
+ var chat = {
+ "type": "aichat",
+ "message": message,
+ "translateMessage": true,
+ "translateParameters": ["_player_"],
+ "parameters": {"_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 = "/allies " + markForTranslation("Let me regroup my army and I am with you against %(_player_)s.");
+ else
+ var message = "/allies " + markForTranslation("I am doing the final preparation and I will attack %(_player_)s.");
+ }
+ else
+ {
+ if (other !== undefined)
+ var message = "/allies " + markForTranslation("I cannot help you against %(_player_)s for the time being, as I have another attack foreseen against %(_player_2)s.");
+ else
+ var message = "/allies " + markForTranslation("Sorry, I have not enough soldiers currently, but my next attack will target %(_player_)s.");
+ }
+
+ var chat = {
+ "type": "aichat",
+ "message": message,
+ "translateMessage": true,
+ "translateParameters": ["_player_"],
+ "parameters": {"_player_": player}
+ };
+ if (other !== undefined)
+ {
+ chat.translateParameters.push("_player_2");
+ chat.parameters._player_2 = 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.");
+ var message = "/team " + markForTranslation("Here is a gift for %(_player_)s, make a good use of it.");
else
- var message = "/team " + markForTranslation("I see you are in a bad situation %(name)s, I hope this will help.");
+ var message = "/team " + markForTranslation("I see you are in a bad situation %(_player_)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": ["_player_"],
+ "parameters": {"_player_": player}
+ };
Engine.PostCommand(PlayerID, chat);
};
m.chatRequestTribute = function(gameState, resource)
{
var proba = Math.random();
if (proba < 0.5)
var message = "/team " + markForTranslation("I am in need of %(resource)s, can you help? I will make it up to you.");
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.");
+ var message = "/team " + markForTranslation("I have set up a new route with %(_player_)s. Trading will be profitable for all of us.");
else
- var message = "/team " + markForTranslation("A new trade route is set up with %(name)s. Take your share of the profits");
+ var message = "/team " + markForTranslation("A new trade route is set up with %(_player_)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": ["_player_"],
+ "parameters": {"_player_": player}
+ };
Engine.PostCommand(PlayerID, chat);
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js (revision 16532)
+++ ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js (revision 16533)
@@ -1,295 +1,296 @@
function AIInterface() {}
AIInterface.prototype.Schema =
"";
AIInterface.prototype.EventNames = [
"Create",
"Destroy",
"Attacked",
"RangeUpdate",
"ConstructionFinished",
"TrainingStarted",
"TrainingFinished",
"AIMetadata",
"PlayerDefeated",
"EntityRenamed",
"OwnershipChanged",
"Garrison",
"UnGarrison",
- "TributeExchanged"
+ "TributeExchanged",
+ "AttackRequest"
];
AIInterface.prototype.Init = function()
{
this.events = {};
for each (var i in this.EventNames)
this.events[i] = [];
this.changedEntities = {};
// cache for technology changes;
// this one is PlayerID->TemplateName->{StringForTheValue, ActualValue}
this.changedTemplateInfo = {};
// this is for auras and is EntityID->{StringForTheValue, ActualValue}
this.changedEntityTemplateInfo = {};
this.enabled = true;
};
AIInterface.prototype.Serialize = function()
{
var state = {};
for (var key in this)
{
if (!this.hasOwnProperty(key))
continue;
if (typeof this[key] == "function")
continue;
state[key] = this[key];
}
return state;
};
AIInterface.prototype.Deserialize = function(data)
{
for (var key in data)
{
if (!data.hasOwnProperty(key))
continue;
this[key] = data[key];
}
if (!this.enabled)
this.Disable();
};
/**
* Disable all registering functions for this component
* Gets called in case no AI players are present to save resources
*/
AIInterface.prototype.Disable = function()
{
this.enabled = false;
var nop = function(){};
this.ChangedEntity = nop;
this.PushEvent = nop;
this.OnGlobalPlayerDefeated = nop;
this.OnGlobalEntityRenamed = nop;
this.OnGlobalTributeExchanged = nop;
this.OnTemplateModification = nop;
this.OnValueModification = nop;
};
AIInterface.prototype.GetNonEntityRepresentation = function()
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
// Return the same game state as the GUI uses
var state = cmpGuiInterface.GetSimulationState(-1);
// Add some extra AI-specific data
// add custom events and reset them for the next turn
state.events = {};
for each (var i in this.EventNames)
{
state.events[i] = this.events[i];
this.events[i] = [];
}
return state;
};
AIInterface.prototype.GetRepresentation = function()
{
var state = this.GetNonEntityRepresentation();
// Add entity representations
Engine.ProfileStart("proxy representations");
state.entities = {};
for (var id in this.changedEntities)
{
var aiProxy = Engine.QueryInterface(+id, IID_AIProxy);
if (aiProxy)
state.entities[id] = aiProxy.GetRepresentation();
}
this.changedEntities = {};
Engine.ProfileStop();
state.changedTemplateInfo = this.changedTemplateInfo;
this.changedTemplateInfo = {};
state.changedEntityTemplateInfo = this.changedEntityTemplateInfo;
this.changedEntityTemplateInfo = {};
return state;
};
// Intended to be called first, during the map initialization: no caching
AIInterface.prototype.GetFullRepresentation = function(flushEvents)
{
var state = this.GetNonEntityRepresentation();
if (flushEvents)
for each (var i in this.EventNames)
state.events[i] = [];
// Add entity representations
Engine.ProfileStart("proxy representations");
state.entities = {};
// all entities are changed in the initial state.
for (var id in this.changedEntities)
{
var aiProxy = Engine.QueryInterface(+id, IID_AIProxy);
if (aiProxy)
state.entities[id] = aiProxy.GetFullRepresentation();
}
Engine.ProfileStop();
state.changedTemplateInfo = this.changedTemplateInfo;
this.changedTemplateInfo = {};
state.changedEntityTemplateInfo = this.changedEntityTemplateInfo;
this.changedEntityTemplateInfo = {};
return state;
};
AIInterface.prototype.ChangedEntity = function(ent)
{
this.changedEntities[ent] = 1;
};
// AIProxy sets up a load of event handlers to capture interesting things going on
// in the world, which we will report to AI. Handle those, and add a few more handlers
// for events that AIProxy won't capture.
AIInterface.prototype.PushEvent = function(type, msg)
{
if (this.events[type] === undefined)
warn("Tried to push unknown event type " + type +", please add it to AIInterface.js");
this.events[type].push(msg);
};
AIInterface.prototype.OnGlobalPlayerDefeated = function(msg)
{
this.events["PlayerDefeated"].push(msg);
};
AIInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
var cmpMirage = Engine.QueryInterface(msg.entity, IID_Mirage);
if (!cmpMirage)
this.events["EntityRenamed"].push(msg);
};
AIInterface.prototype.OnGlobalTributeExchanged = function(msg)
{
this.events["TributeExchanged"].push(msg);
};
// When a new technology is researched, check which templates it affects,
// and send the updated values to the AI.
// this relies on the fact that any "value" in a technology can only ever change
// one template value, and that the naming is the same (with / in place of .)
// it's not incredibly fast but it's not incredibly slow.
AIInterface.prototype.OnTemplateModification = function(msg)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
if (!this.templates)
{
this.templates = cmpTemplateManager.FindAllTemplates(false);
for (let i = 0; i < this.templates.length; ++i)
{
// remove templates that we obviously don't care about.
if (this.templates[i].startsWith("skirmish/"))
this.templates.splice(i--,1);
else
{
let template = cmpTemplateManager.GetTemplateWithoutValidation(this.templates[i]);
if (!template || !template.Identity || !template.Identity.Civ)
this.templates.splice(i--,1);
}
}
}
for (let name of this.templates)
{
let template = cmpTemplateManager.GetTemplateWithoutValidation(name);
for (let valName of msg.valueNames)
{
// let's get the base template value.
let strings = valName.split("/");
let item = template;
let ended = true;
for (let str of strings)
{
if (item !== undefined && item[str] !== undefined)
item = item[str];
else
ended = false;
}
if (!ended)
continue;
// item now contains the template value for this.
let oldValue = +item;
let newValue = ApplyValueModificationsToTemplate(valName, oldValue, msg.player, template);
// Apply the same roundings as in the components
if (valName === "Health/Max" || valName === "Player/MaxPopulation")
newValue = Math.round(newValue);
// TODO in some cases, we can have two opposite changes which bring us to the old value,
// and we should keep it. But how to distinguish it ?
if(newValue != oldValue)
continue;
if (!this.changedTemplateInfo[msg.player])
this.changedTemplateInfo[msg.player] = {};
if (!this.changedTemplateInfo[msg.player][name])
this.changedTemplateInfo[msg.player][name] = [{"variable": valName, "value": newValue}];
else
this.changedTemplateInfo[msg.player][name].push({"variable": valName, "value": newValue});
}
}
};
AIInterface.prototype.OnGlobalValueModification = function(msg)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let ent of msg.entities)
{
let templateName = cmpTemplateManager.GetCurrentTemplateName(ent);
// if there's no template name, the unit is probably killed, ignore it.
if (!templateName || !templateName.length)
continue;
let template = cmpTemplateManager.GetTemplateWithoutValidation(templateName);
for (let valName of msg.valueNames)
{
// let's get the base template value.
let strings = valName.split("/");
let item = template;
let ended = true;
for (let str of strings)
{
if (item !== undefined && item[str] !== undefined)
item = item[str];
else
ended = false;
}
if (!ended)
continue;
// "item" now contains the unmodified template value for this.
let oldValue = +item;
let newValue = ApplyValueModificationsToEntity(valName, oldValue, ent);
// Apply the same roundings as in the components
if (valName === "Health/Max" || valName === "Player/MaxPopulation")
newValue = Math.round(newValue);
// TODO in some cases, we can have two opposite changes which bring us to the old value,
// and we should keep it. But how to distinguish it ?
if (newValue == oldValue)
continue;
if (!this.changedEntityTemplateInfo[ent])
this.changedEntityTemplateInfo[ent] = [{"variable": valName, "value": newValue}];
else
this.changedEntityTemplateInfo[ent].push({"variable": valName, "value": newValue});
}
}
};
Engine.RegisterSystemComponentType(IID_AIInterface, "AIInterface", AIInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 16532)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 16533)
@@ -1,1597 +1,1619 @@
// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
function ProcessCommand(player, cmd)
{
// Do some basic checks here that commanding player is valid
var data = {};
data.cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!data.cmpPlayerMan || player < 0)
return;
data.playerEnt = data.cmpPlayerMan.GetPlayerByID(player);
if (data.playerEnt == INVALID_ENTITY)
return;
data.cmpPlayer = Engine.QueryInterface(data.playerEnt, IID_Player);
if (!data.cmpPlayer)
return;
data.controlAllUnits = data.cmpPlayer.CanControlAllUnits();
if (cmd.entities)
data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// Now handle various commands
if (commands[cmd.type])
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("PlayerCommand", {"player": player, "cmd": cmd});
commands[cmd.type](player, cmd, data);
}
else
error("Invalid command: unknown command type: "+uneval(cmd));
}
var commands = {
"debug-print": function(player, cmd, data)
{
print(cmd.message);
},
"chat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": cmd.type, "players": [player], "message": cmd.message});
},
"aichat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
var notification = { "players": [player] };
for (var key in cmd)
notification[key] = cmd[key];
cmpGuiInterface.PushNotification(notification);
},
"cheat": function(player, cmd, data)
{
Cheat(cmd);
},
"quit": function(player, cmd, data)
{
// Let the AI exit the game for testing purposes
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "quit", "players": [player]});
},
"diplomacy": function(player, cmd, data)
{
switch(cmd.to)
{
case "ally":
data.cmpPlayer.SetAlly(cmd.player);
break;
case "neutral":
data.cmpPlayer.SetNeutral(cmd.player);
break;
case "enemy":
data.cmpPlayer.SetEnemy(cmd.player);
break;
default:
warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "diplomacy", "players": [player], "player1": cmd.player, "status": cmd.to});
},
"tribute": function(player, cmd, data)
{
data.cmpPlayer.TributeResource(cmd.player, cmd.amounts);
},
"control-all": function(player, cmd, data)
{
data.cmpPlayer.SetControlAllUnits(cmd.flag);
},
"reveal-map": function(player, cmd, data)
{
// Reveal the map for all players, not just the current player,
// primarily to make it obvious to everyone that the player is cheating
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
},
"walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
});
},
"walk-to-range": function(player, cmd, data)
{
// Only used by the AI
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(cmpUnitAI)
cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued);
}
},
"attack-walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued);
});
},
"attack": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanAttack for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Attack(cmd.target, cmd.queued);
});
},
"heal": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
}
// See UnitAI.CanHeal for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Heal(cmd.target, cmd.queued);
});
},
"repair": function(player, cmd, data)
{
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanRepair for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
});
},
"gather": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
}
// See UnitAI.CanGather for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Gather(cmd.target, cmd.queued);
});
},
"gather-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued);
});
},
"returnresource": function(player, cmd, data)
{
// Check dropsite is owned by player
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
}
// See UnitAI.CanReturnResource for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
});
},
"back-to-work": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(!cmpUnitAI || !cmpUnitAI.BackToWork())
notifyBackToWorkFailure(player);
}
},
"remove-guard": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(cmpUnitAI)
cmpUnitAI.RemoveGuard();
}
},
"train": function(player, cmd, data)
{
// Check entity limits
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(cmd.template);
var unitCategory = null;
if (template.TrainingRestrictions)
unitCategory = template.TrainingRestrictions.Category;
// Verify that the building(s) can be controlled by the player
if (data.entities.length <= 0)
{
if (g_DebugCommands)
warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
for each (var ent in data.entities)
{
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));
continue;
}
}
var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
if (!cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
continue;
}
var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
// Check if the building can train the unit
// TODO: the AI API does not take promotion technologies into account for the list
// of trainable units (taken directly from the unit template). Here is a temporary fix.
if (queue && data.cmpPlayer.IsAI())
{
var list = queue.GetEntitiesList();
if (list.indexOf(cmd.template) === -1 && cmd.promoted)
{
for (var promoted of cmd.promoted)
{
if (list.indexOf(promoted) === -1)
continue;
cmd.template = promoted;
break;
}
}
}
if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
if ("metadata" in cmd)
queue.AddBatch(cmd.template, "unit", +cmd.count, cmd.metadata);
else
queue.AddBatch(cmd.template, "unit", +cmd.count);
}
},
"research": function(player, cmd, data)
{
// Verify that the building can be controlled by the player
if (!CanControlUnit(cmd.entity, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: research building cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
if (!cmpTechnologyManager.CanResearch(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.AddBatch(cmd.template, "technology");
},
"stop-production": function(player, cmd, data)
{
// Verify that the building can be controlled by the player
if (!CanControlUnit(cmd.entity, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: production building cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.RemoveBatch(cmd.id);
},
"construct": function(player, cmd, data)
{
TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"construct-wall": function(player, cmd, data)
{
TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"delete-entities": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply || !cmpResourceSupply.GetKillBeforeGather())
cmpHealth.Kill();
}
else
Engine.DestroyEntity(ent);
}
},
"set-rallypoint": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
if (!cmd.queued)
cmpRallyPoint.Unset();
cmpRallyPoint.AddPosition(cmd.x, cmd.z);
cmpRallyPoint.AddData(cmd.data);
}
}
},
"unset-rallypoint": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Reset();
}
},
"defeat-player": function(player, cmd, data)
{
// Send "OnPlayerDefeated" message to player
Engine.PostMessage(data.playerEnt, MT_PlayerDefeated, { "playerId": player } );
},
"garrison": function(player, cmd, data)
{
// Verify that the building can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Garrison(cmd.target, cmd.queued);
});
},
"guard": function(player, cmd, data)
{
// Verify that the target can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Guard(cmd.target, cmd.queued);
});
},
"stop": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Stop(cmd.queued);
});
},
"unload": function(player, cmd, data)
{
// Verify that the building can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
var notUngarrisoned = 0;
// The owner can ungarrison every garrisoned unit
if (IsOwnedByPlayer(player, cmd.garrisonHolder))
data.entities = cmd.entities;
for each (var ent in data.entities)
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
notUngarrisoned++;
if (notUngarrisoned != 0)
notifyUnloadFailure(player, cmd.garrisonHolder)
},
"unload-template": function(player, cmd, data)
{
var index = cmd.template.indexOf("&"); // Templates for garrisoned units are extended
if (index == -1)
return;
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
// Only the owner of the garrisonHolder may unload entities from any owners
if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits
&& player != +cmd.template.slice(1,index))
continue;
if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.all))
notifyUnloadFailure(player, garrisonHolder);
}
}
},
"unload-all-own": function(player, cmd, data)
{
var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllOwn())
notifyUnloadFailure(player, garrisonHolder)
}
},
"unload-all": function(player, cmd, data)
{
var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
notifyUnloadFailure(player, garrisonHolder)
}
},
"increase-alert-level": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (!cmpAlertRaiser || !cmpAlertRaiser.IncreaseAlertLevel())
notifyAlertFailure(player);
}
},
"alert-end": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.EndOfAlert();
}
},
"formation": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd.name).forEach(function(cmpUnitAI) {
cmpUnitAI.MoveIntoFormation(cmd);
});
},
"promote": function(player, cmd, data)
{
// No need to do checks here since this is a cheat anyway
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "chat", "players": [player], "message": "(Cheat - promoted units)"});
for each (var ent in cmd.entities)
{
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
}
},
"stance": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && !cmpUnitAI.IsTurret())
cmpUnitAI.SwitchToStance(cmd.name);
}
},
"wall-to-gate": function(player, cmd, data)
{
for each (var ent in data.entities)
{
TryTransformWallToGate(ent, data.cmpPlayer, cmd.template);
}
},
"lock-gate": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
{
if (cmd.lock)
cmpGate.LockGate();
else
cmpGate.UnlockGate();
}
}
},
"setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
});
},
"select-required-goods": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
cmpTrader.SetRequiredGoods(cmd.requiredGoods);
}
},
"set-trading-goods": function(player, cmd, data)
{
data.cmpPlayer.SetTradingGoods(cmd.tradingGoods);
},
"barter": function(player, cmd, data)
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
cmpBarter.ExchangeResources(data.playerEnt, cmd.sell, cmd.buy, cmd.amount);
},
"set-shading-color": function(player, cmd, data)
{
// Debug command to make an entity brightly colored
for each (var ent in cmd.entities)
{
var cmpVisual = Engine.QueryInterface(ent, IID_Visual)
if (cmpVisual)
cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0) // alpha isn't used so just send 0
}
},
"pack": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmd.pack)
cmpUnitAI.Pack(cmd.queued);
else
cmpUnitAI.Unpack(cmd.queued);
}
}
},
"cancel-pack": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmd.pack)
cmpUnitAI.CancelPack(cmd.queued);
else
cmpUnitAI.CancelUnpack(cmd.queued);
}
}
},
+
+ "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": "/allies " + markForTranslation("Attack against %(_player_)s requested."),
+ "translateParameters": ["_player_"],
+ "parameters": {"_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
// message to any component you like.
},
};
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player, garrisonHolder)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Unable to ungarrison unit(s)" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Sends a GUI notification about worker(s) that failed to go back to work.
*/
function notifyBackToWorkFailure(player)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Some unit(s) can't go back to work" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Sends a GUI notification about Alerts that failed to be raised
*/
function notifyAlertFailure(player)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "You can't raise the alert to a higher level !" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Get some information about the formations used by entities.
* The entities must have a UnitAI component.
*/
function ExtractFormations(ents)
{
var entities = []; // subset of ents that have UnitAI
var members = {}; // { formationentity: [ent, ent, ...], ... }
for each (var ent in ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var fid = cmpUnitAI.GetFormationController();
if (fid != INVALID_ENTITY)
{
if (!members[fid])
members[fid] = [];
members[fid].push(ent);
}
entities.push(ent);
}
var ids = [ id for (id in members) ];
return { "entities": entities, "members": members, "ids": ids };
}
/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
function GetDockAngle(template, x, z)
{
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
return undefined;
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
else if (template.Footprint.Circle)
halfSize = template.Footprint.Circle["@radius"];
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = x - d*Math.sin(angle);
var nz = z + d*Math.cos(angle);
if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
waterPoints.push(i);
}
var consec = [];
var length = waterPoints.length;
if (!length)
continue;
for (var i = 0; i < length; ++i)
{
var count = 0;
for (var j = 0; j < (length-1); ++j)
{
if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length])
++count;
else
break;
}
consec[i] = count;
}
var start = 0;
var count = 0;
for (var c in consec)
{
if (consec[c] > count)
{
start = c;
count = consec[c];
}
}
// If we've found a shoreline, stop searching
if (count != numPoints-1)
return -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI);
}
return undefined;
}
/**
* Attempts to construct a building using the specified parameters.
* Returns true on success, false on failure.
*/
function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
{
// Message structure:
// {
// "type": "construct",
// "entities": [...], // entities that will be ordered to construct the building (if applicable)
// "template": "...", // template name of the entity being constructed
// "x": ...,
// "z": ...,
// "angle": ...,
// "metadata": "...", // AI metadata of the building
// "actorSeed": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
// "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
// "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. May be INVALID_ENTITY.
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check whether we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
return false;
var foundationTemplate = "foundation|" + cmd.template;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity(foundationTemplate);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
return false;
}
// If it's a dock, get the right angle.
var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateMgr.GetTemplate(cmd.template);
if (template.BuildRestrictions.Category === "Dock")
{
var angle = GetDockAngle(template, cmd.x, cmd.z);
if (angle !== undefined)
cmd.angle = angle;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(cmd.angle);
// Set the obstruction control group if needed
if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
{
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
// primary control group must always be valid
if (cmd.obstructionControlGroup)
{
if (cmd.obstructionControlGroup <= 0)
warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
}
if (cmd.obstructionControlGroup2)
cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (cmpBuildRestrictions)
{
var ret = cmpBuildRestrictions.CheckPlacement();
if (!ret.success)
{
if (g_DebugCommands)
{
warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
ret.players = [player];
cmpGuiInterface.PushNotification(ret);
// Remove the foundation because the construction was aborted
// move it out of world because it's not destroyed immediately.
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
}
else
error("cmpBuildRestrictions not defined");
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (!cmpEntityLimits || !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
{
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
}
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
{
warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "players": [player], "message": "Building's technology requirements are not met." });
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
}
// We need the cost after tech modifications
// To calculate this with an entity requires ownership, so use the template instead
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(foundationTemplate);
var costs = {};
for (var r in template.Cost.Resources)
{
costs[r] = +template.Cost.Resources[r];
if (cmpTechnologyManager)
costs[r] = cmpTechnologyManager.ApplyModificationsTemplate("Cost/Resources/"+r, costs[r], template);
}
if (!cmpPlayer.TrySubtractResources(costs))
{
if (g_DebugCommands)
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(ent);
cmpPosition.MoveOutOfWorld();
return false;
}
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual && cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// send Metadata info if any
if (cmd.metadata)
Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued
});
}
return ent;
}
function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
{
// 'cmd' message structure:
// {
// "type": "construct-wall",
// "entities": [...], // entities that will be ordered to construct the wall (if applicable)
// "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
// {
// "template": "...", // one of the templates from the wallset
// "x": ...,
// "z": ...,
// "angle": ...,
// },
// ...
// ],
// "wallSet": {
// "templates": {
// "tower": // tower template name
// "long": // long wall segment template name
// ... // etc.
// },
// "maxTowerOverlap": ...,
// "minTowerOverlap": ...,
// },
// "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall
// "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
// }
if (cmd.pieces.length <= 0)
return;
if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
return;
}
if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
return;
}
// Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
// and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
// groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
// towers in the case of snapping). The towers themselves all keep their default unique control groups.
// To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
// it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
// first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
// the first tower encountered towards the ending side of the wall (if any).
// We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
// wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
//
// FIRST PASS:
// - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
// cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
// as the primary control group, thus allowing it to be built overlapping the previous piece.
// - If we encounter a new tower along the way (which will gain its own control group), do the following:
// o First build it using temporarily the same control group of the previous (non-tower) piece
// o Set the previous piece's secondary control group to the tower's entity ID
// o Restore the primary control group of the constructed tower back its original (unique) value.
// The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
// to be placed while overlapping the previous piece.
//
// SECOND PASS:
// - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
// time registering the right neighbouring tower in each non-tower piece.
// first pass; L -> R
var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
// the first wall piece can be built while overlapping it.
if (cmd.startSnappedEntity)
{
var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
if (!cmpSnappedStartObstruction)
{
error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
//warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
}
var i = 0;
for (; i < cmd.pieces.length; ++i)
{
var piece = cmd.pieces[i];
// All wall pieces after the first must be queued.
if (i > 0 && !cmd.queued)
cmd.queued = true;
// 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
// start position snapping (implying that the first entity we build must be a tower)
if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
{
if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
{
error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
break;
}
}
var constructPieceCmd = {
"type": "construct",
"entities": cmd.entities,
"template": piece.template,
"x": piece.x,
"z": piece.z,
"angle": piece.angle,
"autorepair": cmd.autorepair,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued,
// Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
// using the control group of the last tower (see comments above).
"obstructionControlGroup": lastTowerControlGroup,
};
// If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
// control group directly at construction time (instead of setting it in the second pass) to allow it to be built
// while overlapping the snapped entity.
if (i == cmd.pieces.length - 1 && cmd.endSnappedEntity)
{
var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (cmpEndSnappedObstruction)
constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
}
var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
if (pieceEntityId)
{
// wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
piece.ent = pieceEntityId;
// if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
if (piece.template == cmd.wallSet.templates.tower)
{
var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
var newTowerControlGroup = pieceEntityId;
if (i > 0)
{
//warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup);
var cmpPreviousObstruction = Engine.QueryInterface(cmd.pieces[i-1].ent, IID_Obstruction);
// TODO: ensure that cmpPreviousObstruction exists
// TODO: ensure that the previous obstruction does not yet have a secondary control group set
cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
}
// TODO: ensure that cmpTowerObstruction exists
cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
lastTowerIndex = i;
lastTowerControlGroup = newTowerControlGroup;
}
}
else
{
// failed to build wall piece, abort
i = j + 1; // compensate for the -1 subtracted by lastBuiltPieceIndex below
break;
}
}
var lastBuiltPieceIndex = i - 1;
var wallComplete = (lastBuiltPieceIndex == cmd.pieces.length - 1);
// At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
// Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
// as their secondary control groups.
lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// only start off with the ending side's snapped tower's control group if we were able to build the entire wall
if (cmd.endSnappedEntity && wallComplete)
{
var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (!cmpSnappedEndObstruction)
{
error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
}
for (var j = lastBuiltPieceIndex; j >= 0; --j)
{
var piece = cmd.pieces[j];
if (!piece.ent)
{
error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
continue;
}
var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
if (!cmpPieceObstruction)
{
error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
continue;
}
if (piece.template == cmd.wallSet.templates.tower)
{
// encountered a tower entity, update the last tower control group
lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
}
else
{
// Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
// Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
// dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
if (existingSecondaryControlGroup == INVALID_ENTITY)
{
if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
{
cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
}
}
else if (existingSecondaryControlGroup != lastTowerControlGroup)
{
error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
break;
}
}
}
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
var formation = ExtractFormations(ents);
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents, player, formationTemplate)
{
// If an individual was selected, remove it from any formation
// and command it individually
if (ents.length == 1)
{
// Skip unit if it has no UnitAI
var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
RemoveFromFormation(ents);
return [ cmpUnitAI ];
}
// Separate out the units that don't support the chosen formation
var formedEnts = [];
var nonformedUnitAIs = [];
for each (var ent in ents)
{
// Skip units with no UnitAI or no position
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
continue;
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
var nullFormation = (formationTemplate || cmpUnitAI.GetLastFormationTemplate()) == "formations/null";
if (!nullFormation && cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "formations/line_closed"))
formedEnts.push(ent);
else
{
if (nullFormation)
cmpUnitAI.SetLastFormationTemplate("formations/null");
nonformedUnitAIs.push(cmpUnitAI);
}
}
if (formedEnts.length == 0)
{
// No units support the foundation - return all the others
return nonformedUnitAIs;
}
// Find what formations the formationable selected entities are currently in
var formation = ExtractFormations(formedEnts);
var formationUnitAIs = [];
if (formation.ids.length == 1)
{
// Selected units either belong to this formation or have no formation
// Check that all its members are selected
var fid = formation.ids[0];
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length
&& cmpFormation.GetMemberCount() == formation.entities.length)
{
cmpFormation.DeleteTwinFormations();
// The whole formation was selected, so reuse its controller for this command
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
cmpFormation.LoadFormation(formationTemplate);
}
}
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller
// Remove selected units from their current formation
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
var formationSeparation = 60;
var clusters = ClusterEntities(formation.entities, formationSeparation);
var formationEnts = [];
for each (var cluster in clusters)
{
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
// get the most recently used formation, or default to line closed
var lastFormationTemplate = undefined;
for each (var ent in cluster)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
var template = cmpUnitAI.GetLastFormationTemplate();
if (lastFormationTemplate === undefined)
{
lastFormationTemplate = template;
}
else if (lastFormationTemplate != template)
{
lastFormationTemplate = undefined;
break;
}
}
}
if (lastFormationTemplate && CanMoveEntsIntoFormation(cluster, lastFormationTemplate))
formationTemplate = lastFormationTemplate;
else
formationTemplate = "formations/line_closed";
}
// Create the new controller
var formationEnt = Engine.AddEntity(formationTemplate);
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
for each (var ent in formationEnts)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
}
}
return nonformedUnitAIs.concat(formationUnitAIs);
}
/**
* Group a list of entities in clusters via single-links
*/
function ClusterEntities(ents, separationDistance)
{
var clusters = [];
if (!ents.length)
return clusters;
var distSq = separationDistance * separationDistance;
var positions = [];
// triangular matrix with the (squared) distances between the different clusters
// the other half is not initialised
var matrix = [];
for (var i = 0; i < ents.length; i++)
{
matrix[i] = [];
clusters.push([ents[i]]);
var cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
positions.push(cmpPosition.GetPosition2D());
for (var j = 0; j < i; j++)
matrix[i][j] = positions[i].distanceToSquared(positions[j]);
}
while (clusters.length > 1)
{
// search two clusters that are closer than the required distance
var smallDist = Infinity;
var closeClusters = undefined;
for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i)
for (var j = i - 1; j >= 0 && !closeClusters; --j)
if (matrix[i][j] < distSq)
closeClusters = [i,j];
// if no more close clusters found, just return all found clusters so far
if (!closeClusters)
return clusters;
// make a new cluster with the entities from the two found clusters
var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
// calculate the minimum distance between the new cluster and all other remaining
// clusters by taking the minimum of the two distances.
var distances = [];
for (var i = 0; i < clusters.length; i++)
{
if (i == closeClusters[1] || i == closeClusters[0])
continue;
var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]];
var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]];
distances.push(Math.min(dist1, dist2));
}
// remove the rows and columns in the matrix for the merged clusters,
// and the clusters themselves from the cluster list
clusters.splice(closeClusters[0],1);
clusters.splice(closeClusters[1],1);
matrix.splice(closeClusters[0],1);
matrix.splice(closeClusters[1],1);
for (var i = 0; i < matrix.length; i++)
{
if (matrix[i].length > closeClusters[0])
matrix[i].splice(closeClusters[0],1);
if (matrix[i].length > closeClusters[1])
matrix[i].splice(closeClusters[1],1);
}
// add a new row of distances to the matrix and the new cluster
clusters.push(newCluster);
matrix.push(distances);
}
return clusters;
}
function GetFormationRequirements(formationTemplate)
{
var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempManager.GetTemplate(formationTemplate);
if (!template.Formation)
return false;
return {"minCount": +template.Formation.RequiredMemberCount};
}
function CanMoveEntsIntoFormation(ents, formationTemplate)
{
// TODO: should check the player's civ is allowed to use this formation
// See simulation/components/Player.js GetFormations() for a list of all allowed formations
var requirements = GetFormationRequirements(formationTemplate);
if (!requirements)
return false;
var count = 0;
var reqClasses = requirements.classesRequired || [];
for each (var ent in ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate))
continue;
count++;
}
return count >= requirements.minCount;
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player
* or control all units is activated, else false
*/
function CanControlUnit(entity, player, controlAll)
{
return (IsOwnedByPlayer(player, entity) || controlAll);
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player
* or the entity is owned by an mutualAlly
* or control all units is activated, else false
*/
function CanControlUnitOrIsAlly(entity, player, controlAll)
{
return (IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll);
}
/**
* Filter entities which the player can control
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(function(ent) { return CanControlUnit(ent, player, controlAll);} );
}
/**
* Filter entities which the player can control or are mutualAlly
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
return entities.filter(function(ent) { return CanControlUnitOrIsAlly(ent, player, controlAll);} );
}
/**
* Try to transform a wall to a gate
*/
function TryTransformWallToGate(ent, cmpPlayer, template)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
return;
// Check if this is a valid long wall segment
if (!cmpIdentity.HasClass("LongWall"))
{
if (g_DebugCommands)
warn("Invalid command: invalid wall conversion to gate for player "+player+": "+uneval(cmd));
return;
}
var civ = cmpIdentity.GetCiv();
var gate = Engine.AddEntity(template);
var cmpCost = Engine.QueryInterface(gate, IID_Cost);
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
if (g_DebugCommands)
warn("Invalid command: convert gate cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(gate);
return;
}
ReplaceBuildingWith(ent, gate);
}
/**
* Unconditionally replace a building with another one
*/
function ReplaceBuildingWith(ent, building)
{
// Move the building to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position);
var pos = cmpPosition.GetPosition2D();
cmpBuildingPosition.JumpTo(pos.x, pos.y);
var rot = cmpPosition.GetRotation();
cmpBuildingPosition.SetYRotation(rot.y);
cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
// Copy ownership
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
// Copy control groups
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
// Copy health level from the old entity to the new
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
var healthFraction = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
var buildingHitpoints = Math.round(cmpBuildingHealth.GetMaxHitpoints() * healthFraction);
cmpBuildingHealth.SetHitpoints(buildingHitpoints);
PlaySound("constructed", building);
Engine.PostMessage(ent, MT_ConstructionFinished,
{ "entity": ent, "newentity": building });
Engine.BroadcastMessage(MT_EntityRenamed, { entity: ent, newentity: building });
Engine.DestroyEntity(ent);
}
Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Engine.RegisterGlobal("commands", commands);