Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 15613) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 15614) @@ -1,692 +1,698 @@ // 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 = parseJSONData("simulation/data/cheats/"+fileName+".json"); 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 } if (notification.type == "aichat") message["translate"] = true; 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 }); // If the diplomacy panel is open refresh it. if (isDiplomacyOpen) openDiplomacy(); }, "diplomacy": function(notification, player) { addChatMessage({ "type": "diplomacy", "player": player, "player1": notification.player1, "status": notification.status }); // If the diplomacy panel is open refresh it. if (isDiplomacyOpen) openDiplomacy(); }, "quit": function(notification, player) { exit(); // TODO this doesn't work anymore }, "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 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) { 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; } } if (!isCheat) { 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 var amounts = Object.keys(msg.amounts) .filter(function (type) { return msg.amounts[type] > 0; }) .map(function (type) { return msg.amounts[type] + " " + type; }); if (amounts.length > 1) { var 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 use message. 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; 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 "/enemy": // 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 && sender != Engine.GetPlayerID()) msg.hide = true; else msg.context = translate("Enemy"); } 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/maps/random/survivalofthefittest.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest.json (revision 15613) +++ ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest.json (revision 15614) @@ -1,16 +1,15 @@ { "settings" : { "Name" : "Survival of the Fittest", "Script" : "survivalofthefittest.js", "Description" : "IMPORTANT NOTE: AI PLAYERS WONT WORK WITH THIS MAP\n\nProtect your base against endless waves of enemies. Use your woman citizen to gather resources. The last player remaining will be the winner.", "BaseTerrain" : ["medit_sea_depths"], "BaseHeight" : 30, "CircularMap" : true, - "Keywords": ["demo"], "TriggerScripts": [ "scripts/TriggerHelper.js", "random/survivalofthefittest_triggers.js" ], "XXXXXX" : "Optionally define other things here, like we would for a scenario" } } Index: ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js (revision 15613) +++ ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js (revision 15614) @@ -1,159 +1,171 @@ var treasures = ["gaia/special_treasure_food_barrel", "gaia/special_treasure_food_bin", "gaia/special_treasure_food_crate", "gaia/special_treasure_food_jars", "gaia/special_treasure_metal", "gaia/special_treasure_stone", "gaia/special_treasure_wood", "gaia/special_treasure_wood", "gaia/special_treasure_wood"]; var attackerEntityTemplates = ["units/athen_champion_infantry", "units/athen_champion_marine", "units/athen_champion_ranged", "units/brit_champion_cavalry", "units/brit_champion_infantry", "units/cart_champion_cavalry", "units/cart_champion_elephant", "units/cart_champion_infantry", "units/cart_champion_pikeman", "units/gaul_champion_cavalry", "units/gaul_champion_fanatic", "units/gaul_champion_infantry", "units/iber_champion_cavalry", "units/iber_champion_infantry", "units/mace_champion_cavalry", "units/mace_champion_infantry_a", "units/mace_champion_infantry_e", "units/maur_champion_chariot", "units/maur_champion_elephant", "units/maur_champion_infantry", "units/maur_champion_maiden", "units/maur_champion_maiden_archer", "units/pers_champion_cavalry", "units/pers_champion_infantry", "units/ptol_champion_cavalry", "units/ptol_champion_elephant", "units/rome_champion_cavalry", "units/rome_champion_infantry", "units/sele_champion_cavalry", "units/sele_champion_chariot", "units/sele_champion_elephant", "units/sele_champion_infantry_pikeman", "units/sele_champion_infantry_swordsman", "units/spart_champion_infantry_pike", "units/spart_champion_infantry_spear", "units/spart_champion_infantry_sword"]; Trigger.prototype.StartAnEnemyWave = function() { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var attackerEntity = attackerEntityTemplates[Math.floor(Math.random() * attackerEntityTemplates.length)]; var attackerCount = Math.round(cmpTimer.GetTime() / 180000); // A soldier for each 3 minutes of the game. Should be waves of 20 soldiers after an hour // spawn attackers var attackers = TriggerHelper.SpawnUnitsFromTriggerPoints("A", attackerEntity, attackerCount, 0); for (var origin in attackers) { var cmpPlayer = QueryOwnerInterface(+origin, IID_Player); if (cmpPlayer.GetState() != "active") continue; var cmpPosition = Engine.QueryInterface(this.playerCivicCenter[cmpPlayer.GetPlayerID()], IID_Position); // this shouldn't happen if the player is still active if (!cmpPosition || !cmpPosition.IsInWorld) continue; // store the x and z coordinates in the command var cmd = cmpPosition.GetPosition(); cmd.type = "attack-walk"; cmd.entities = attackers[origin]; cmd.queued = true; // send the attack-walk command ProcessCommand(0, cmd); } var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [1,2,3,4,5,6,7,8], "message": markForTranslation("An enemy wave is attacking!"), "translateMessage": true }); cmpTrigger.DoAfterDelay(180000, "StartAnEnemyWave", {}); // The next wave will come in 3 minutes } Trigger.prototype.InitGame = function() { var numberOfPlayers = TriggerHelper.GetNumberOfPlayers(); // Find all of the civic centers for (var i = 1; i < numberOfPlayers; ++i) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); // Get all of each player's entities for each (var entity in playerEntities) { if (TriggerHelper.EntityHasClass(entity, "CivilCentre")) { cmpTrigger.playerCivicCenter[i] = entity; } } } + // Fix alliances + for (var i = 1; i < numberOfPlayers; ++i) + { + var cmpPlayer = TriggerHelper.GetPlayerComponent(i); + for (var j = 1; j < numberOfPlayers; ++j) + if (i != j) + cmpPlayer.SetAlly(j); + cmpPlayer.SetLockTeams(true); + } + // make gaia black TriggerHelper.GetPlayerComponent(0).SetColour(0, 0, 0); // Place the treasures this.PlaceTreasures(); } Trigger.prototype.PlaceTreasures = function() { var triggerPoints = cmpTrigger.GetTriggerPoints("B"); for (var point of triggerPoints) { var template = treasures[Math.floor(Math.random() * treasures.length)] TriggerHelper.SpawnUnits(point, template, 1, 0); } cmpTrigger.DoAfterDelay(4*60*1000, "PlaceTreasures", {}); //Place more treasures after 4 minutes } Trigger.prototype.InitializeEnemyWaves = function() { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [1,2,3,4,5,6,7,8], "message": markForTranslation("The first wave will start in 15 minutes!"), "translateMessage": true }); cmpTrigger.DoAfterDelay(15*60*1000, "StartAnEnemyWave", {}); } Trigger.prototype.DefeatPlayerOnceCCIsDestroyed = function(data) { // Defeat a player that has lost his civic center if (data.entity == cmpTrigger.playerCivicCenter[data.from] && data.to == -1) + { TriggerHelper.DefeatPlayer(data.from); - // Check if only one player remains. He will be the winner. - var lastPlayerStanding = 0; - var numPlayersStanding = 0; - var numberOfPlayers = TriggerHelper.GetNumberOfPlayers(); - for (var i = 1; i < numberOfPlayers; ++i) - { - if (TriggerHelper.GetPlayerComponent(i).GetState() == "active") + // Check if only one player remains. He will be the winner. + var lastPlayerStanding = 0; + var numPlayersStanding = 0; + var numberOfPlayers = TriggerHelper.GetNumberOfPlayers(); + for (var i = 1; i < numberOfPlayers; ++i) { - lastPlayerStanding = i; - ++numPlayersStanding; + if (TriggerHelper.GetPlayerComponent(i).GetState() == "active") + { + lastPlayerStanding = i; + ++numPlayersStanding; + } } + if (numPlayersStanding == 1) + TriggerHelper.SetPlayerWon(lastPlayerStanding); } - if (numPlayersStanding == 1) - TriggerHelper.SetPlayerWon(lastPlayerStanding); } var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.playerCivicCenter = {}; cmpTrigger.DoAfterDelay(0, "InitGame", {}); cmpTrigger.DoAfterDelay(1000, "InitializeEnemyWaves", {}); cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DefeatPlayerOnceCCIsDestroyed", {"enabled": true}); Index: ps/trunk/binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Builder.js (revision 15613) +++ ps/trunk/binaries/data/mods/public/simulation/components/Builder.js (revision 15614) @@ -1,78 +1,86 @@ function Builder() {} Builder.prototype.Schema = "Allows the unit to construct and repair buildings." + "" + "1.0" + "" + "\n structures/{civ}_barracks\n structures/{civ}_civil_centre\n structures/celt_sb1\n " + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + ""; Builder.prototype.Init = function() { }; Builder.prototype.Serialize = null; // we have no dynamic state to save Builder.prototype.GetEntitiesList = function() { var entities = []; var string = this.template.Entities._string; if (string) { // Replace the "{civ}" codes with this entity's civ ID var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{civ\}/g, cmpIdentity.GetCiv()); entities = string.split(/\s+/); + + // Remove disabled entities + var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player) + var disabledEntities = cmpPlayer.GetDisabledTemplates(); + + for (var i = entities.length - 1; i >= 0; --i) + if (disabledEntities[entities[i]]) + entities.splice(i, 1); } return entities; }; Builder.prototype.GetRange = function() { var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); var max = 2; if (cmpObstruction) max += cmpObstruction.GetUnitRadius(); return { "max": max, "min": 0 }; }; /** * Build/repair the target entity. This should only be called after a successful range check. * It should be called at a rate of once per second. * Returns obj with obj.finished==true if this is a repair and it's fully repaired. */ Builder.prototype.PerformBuilding = function(target) { var rate = ApplyValueModificationsToEntity("Builder/Rate", +this.template.Rate, this.entity); // If it's a foundation, then build it var cmpFoundation = Engine.QueryInterface(target, IID_Foundation); if (cmpFoundation) { cmpFoundation.Build(this.entity, rate); return; } // Otherwise try to repair it var cmpHealth = Engine.QueryInterface(target, IID_Health); if (cmpHealth) { cmpHealth.Repair(this.entity, rate); return; } }; Engine.RegisterComponentType(IID_Builder, "Builder", Builder); Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 15613) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 15614) @@ -1,702 +1,732 @@ function Player() {} Player.prototype.Schema = ""; Player.prototype.Init = function() { this.playerID = undefined; this.name = undefined; // define defaults elsewhere (supporting other languages) this.civ = undefined; this.colour = { "r": 0.0, "g": 0.0, "b": 0.0, "a": 1.0 }; this.popUsed = 0; // population of units owned or trained by this player this.popBonuses = 0; // sum of population bonuses of player's entities this.maxPop = 300; // maximum population this.trainingBlocked = false; // indicates whether any training queue is currently blocked this.resourceCount = { "food": 300, "wood": 300, "metal": 300, "stone": 300 }; this.tradingGoods = [ // goods for next trade-route and its proba in % (the sum of probas must be 100) { "goods": "wood", "proba": 30 }, { "goods": "stone", "proba": 35 }, { "goods": "metal", "proba": 35 } ]; this.team = -1; // team number of the player, players on the same team will always have ally diplomatic status - also this is useful for team emblems, scoring, etc. this.teamsLocked = false; this.state = "active"; // game state - one of "active", "defeated", "won" this.diplomacy = []; // array of diplomatic stances for this player with respect to other players (including gaia and self) this.conquestCriticalEntitiesCount = 0; // number of owned units with ConquestCritical class this.formations = []; this.startCam = undefined; this.controlAllUnits = false; this.isAI = false; this.gatherRateMultiplier = 1; this.cheatsEnabled = false; this.cheatTimeMultiplier = 1; this.heroes = []; this.resourceNames = { "food": markForTranslation("Food"), "wood": markForTranslation("Wood"), "metal": markForTranslation("Metal"), "stone": markForTranslation("Stone"), } + this.disabledTemplates = {}; }; Player.prototype.SetPlayerID = function(id) { this.playerID = id; }; Player.prototype.GetPlayerID = function() { return this.playerID; }; Player.prototype.SetName = function(name) { this.name = name; }; Player.prototype.GetName = function() { return this.name; }; Player.prototype.SetCiv = function(civcode) { this.civ = civcode; }; Player.prototype.GetCiv = function() { return this.civ; }; Player.prototype.SetColour = function(r, g, b) { this.colour = { "r": r/255.0, "g": g/255.0, "b": b/255.0, "a": 1.0 }; }; Player.prototype.GetColour = function() { return this.colour; }; // Try reserving num population slots. Returns 0 on success or number of missing slots otherwise. Player.prototype.TryReservePopulationSlots = function(num) { if (num != 0 && num > (this.GetPopulationLimit() - this.GetPopulationCount())) return num - (this.GetPopulationLimit() - this.GetPopulationCount()); this.popUsed += num; return 0; }; Player.prototype.UnReservePopulationSlots = function(num) { this.popUsed -= num; }; Player.prototype.GetPopulationCount = function() { return this.popUsed; }; Player.prototype.SetPopulationBonuses = function(num) { this.popBonuses = num; }; Player.prototype.AddPopulationBonuses = function(num) { this.popBonuses += num; }; Player.prototype.GetPopulationLimit = function() { return Math.min(this.GetMaxPopulation(), this.popBonuses); }; Player.prototype.SetMaxPopulation = function(max) { this.maxPop = max; }; Player.prototype.GetMaxPopulation = function() { return Math.round(ApplyValueModificationsToPlayer("Player/MaxPopulation", this.maxPop, this.entity)); }; Player.prototype.SetGatherRateMultiplier = function(value) { this.gatherRateMultiplier = value; }; Player.prototype.GetGatherRateMultiplier = function() { return this.gatherRateMultiplier; }; Player.prototype.GetHeroes = function() { return this.heroes; }; Player.prototype.IsTrainingBlocked = function() { return this.trainingBlocked; }; Player.prototype.BlockTraining = function() { this.trainingBlocked = true; }; Player.prototype.UnBlockTraining = function() { this.trainingBlocked = false; }; Player.prototype.SetResourceCounts = function(resources) { if (resources.food !== undefined) this.resourceCount.food = resources.food; if (resources.wood !== undefined) this.resourceCount.wood = resources.wood; if (resources.stone !== undefined) this.resourceCount.stone = resources.stone; if (resources.metal !== undefined) this.resourceCount.metal = resources.metal; }; Player.prototype.GetResourceCounts = function() { return this.resourceCount; }; /** * Add resource of specified type to player * @param type Generic type of resource (string) * @param amount Amount of resource, which should be added (integer) */ Player.prototype.AddResource = function(type, amount) { this.resourceCount[type] += (+amount); }; /** * Add resources to player */ Player.prototype.AddResources = function(amounts) { for (var type in amounts) { this.resourceCount[type] += (+amounts[type]); } }; Player.prototype.GetNeededResources = function(amounts) { // Check if we can afford it all var amountsNeeded = {}; for (var type in amounts) if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type]) amountsNeeded[type] = amounts[type] - Math.floor(this.resourceCount[type]); if (Object.keys(amountsNeeded).length == 0) return undefined; return amountsNeeded; }; Player.prototype.SubtractResourcesOrNotify = function(amounts) { var amountsNeeded = this.GetNeededResources(amounts); // If we don't have enough resources, send a notification to the player if (amountsNeeded) { var parameters = {}; var i = 0; for (var type in amountsNeeded) { i++; parameters["resourceType"+i] = this.resourceNames[type]; parameters["resourceAmount"+i] = amountsNeeded[type]; } var msg = ""; // when marking strings for translations, you need to include the actual string, // not some way to derive the string if (i < 1) warn("Amounts needed but no amounts given?"); else if (i == 1) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s"); else if (i == 2) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s"); else if (i == 3) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s"); else if (i == 4) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s, %(resourceAmount4)s %(resourceType4)s"); else warn("Localisation: Strings are not localised for more than 4 resources"); var notification = { "players": [this.playerID], "message": msg, "parameters": parameters, "translateMessage": true, "translateParameters": { "resourceType1": "withinSentence", "resourceType2": "withinSentence", "resourceType3": "withinSentence", "resourceType4": "withinSentence", }, }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); return false; } // Subtract the resources for (var type in amounts) this.resourceCount[type] -= amounts[type]; return true; }; Player.prototype.TrySubtractResources = function(amounts) { if (!this.SubtractResourcesOrNotify(amounts)) return false; var cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (var type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]); return true; }; Player.prototype.GetNextTradingGoods = function() { var value = 100*Math.random(); var last = this.tradingGoods.length - 1; var sumProba = 0; for (var i = 0; i < last; ++i) { sumProba += this.tradingGoods[i].proba; if (value < sumProba) return this.tradingGoods[i].goods; } return this.tradingGoods[last].goods; }; Player.prototype.GetTradingGoods = function() { var tradingGoods = {}; for each (var resource in this.tradingGoods) tradingGoods[resource.goods] = resource.proba; return tradingGoods; }; Player.prototype.SetTradingGoods = function(tradingGoods) { var sumProba = 0; for (var resource in tradingGoods) sumProba += tradingGoods[resource]; if (sumProba != 100) // consistency check { error("Player.js SetTradingGoods: " + uneval(tradingGoods)); tradingGoods = { "food": 20, "wood":20, "stone":30, "metal":30 }; } this.tradingGoods = []; for (var resource in tradingGoods) this.tradingGoods.push( {"goods": resource, "proba": tradingGoods[resource]} ); }; Player.prototype.GetState = function() { return this.state; }; Player.prototype.SetState = function(newState) { this.state = newState; }; Player.prototype.GetConquestCriticalEntitiesCount = function() { return this.conquestCriticalEntitiesCount; }; Player.prototype.GetTeam = function() { return this.team; }; Player.prototype.SetTeam = function(team) { if (!this.teamsLocked) { this.team = team; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (cmpPlayerManager && this.team != -1) { // Set all team members as allies for (var i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i) { var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(i), IID_Player); if (this.team == cmpPlayer.GetTeam()) { this.SetAlly(i); cmpPlayer.SetAlly(this.playerID); } } } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } }; Player.prototype.SetLockTeams = function(value) { this.teamsLocked = value; }; Player.prototype.GetLockTeams = function() { return this.teamsLocked; }; Player.prototype.GetDiplomacy = function() { return this.diplomacy; }; Player.prototype.SetDiplomacy = function(dipl) { // Should we check for teamsLocked here? this.diplomacy = dipl; Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); }; Player.prototype.SetDiplomacyIndex = function(idx, value) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(idx), IID_Player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; // You can have alliances with other players, if (this.teamsLocked) { // but can't stab your team members in the back if (this.team == -1 || this.team != cmpPlayer.GetTeam()) { // Break alliance or declare war if (Math.min(this.diplomacy[idx],cmpPlayer.diplomacy[this.playerID]) > value) { this.diplomacy[idx] = value; cmpPlayer.SetDiplomacyIndex(this.playerID, value); } else { this.diplomacy[idx] = value; } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } } else { // Break alliance or declare war (worsening of relations is mutual) if (Math.min(this.diplomacy[idx],cmpPlayer.diplomacy[this.playerID]) > value) { // This is duplicated because otherwise we get too much recursion this.diplomacy[idx] = value; cmpPlayer.SetDiplomacyIndex(this.playerID, value); } else { this.diplomacy[idx] = value; } Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID}); } }; Player.prototype.UpdateSharedLos = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var sharedLos = []; for (var i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i) if (this.IsMutualAlly(i)) sharedLos.push(i); cmpRangeManager.SetSharedLos(this.playerID, sharedLos); }; Player.prototype.GetFormations = function() { return this.formations; }; Player.prototype.SetFormations = function(formations) { this.formations = formations; }; Player.prototype.GetStartingCameraPos = function() { return this.startCam.position; }; Player.prototype.GetStartingCameraRot = function() { return this.startCam.rotation; }; Player.prototype.SetStartingCamera = function(pos, rot) { this.startCam = {"position": pos, "rotation": rot}; }; Player.prototype.HasStartingCamera = function() { return (this.startCam !== undefined); }; Player.prototype.SetControlAllUnits = function(c) { this.controlAllUnits = c; }; Player.prototype.CanControlAllUnits = function() { return this.controlAllUnits; }; Player.prototype.SetAI = function(flag) { this.isAI = flag; }; Player.prototype.IsAI = function() { return this.isAI; }; Player.prototype.SetAlly = function(id) { this.SetDiplomacyIndex(id, 1); }; /** * Check if given player is our ally */ Player.prototype.IsAlly = function(id) { return this.diplomacy[id] > 0; }; /** * Check if given player is our ally, and we are its ally */ Player.prototype.IsMutualAlly = function(id) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return false; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(id), IID_Player); return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID); }; Player.prototype.SetEnemy = function(id) { this.SetDiplomacyIndex(id, -1); }; /** * Get all enemies of a given player. */ Player.prototype.GetEnemies = function() { var enemies = []; for (var i = 0; i < this.diplomacy.length; i++) if (this.diplomacy[i] < 0) enemies.push(i); return enemies; }; /** * Check if given player is our enemy */ Player.prototype.IsEnemy = function(id) { return this.diplomacy[id] < 0; }; Player.prototype.SetNeutral = function(id) { this.SetDiplomacyIndex(id, 0); }; /** * Check if given player is neutral */ Player.prototype.IsNeutral = function(id) { return this.diplomacy[id] == 0; }; /** * Keep track of population effects of all entities that * become owned or unowned by this player */ Player.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from != this.playerID && msg.to != this.playerID) return; var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); var cmpCost = Engine.QueryInterface(msg.entity, IID_Cost); var cmpFoundation = Engine.QueryInterface(msg.entity, IID_Foundation); if (msg.from == this.playerID) { if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("ConquestCritical")) this.conquestCriticalEntitiesCount--; if (cmpCost) this.popUsed -= cmpCost.GetPopCost(); if (cmpIdentity && cmpIdentity.HasClass("Hero")) { //Remove from Heroes list var index = this.heroes.indexOf(msg.entity); if (index >= 0) this.heroes.splice(index, 1); } } if (msg.to == this.playerID) { if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("ConquestCritical")) this.conquestCriticalEntitiesCount++; if (cmpCost) this.popUsed += cmpCost.GetPopCost(); if (cmpIdentity && cmpIdentity.HasClass("Hero")) this.heroes.push(msg.entity); } }; Player.prototype.OnPlayerDefeated = function(msg) { this.state = "defeated"; // TODO: Tribute all resources to this player's active allies (if any) // Reassign all player's entities to Gaia var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); // The ownership change is done in two steps so that entities don't hit idle // (and thus possibly look for "enemies" to attack) before nearby allies get // converted to Gaia as well. for each (var entity in entities) { var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); cmpOwnership.SetOwnerQuiet(0); } // With the real ownership change complete, send OwnershipChanged messages. for each (var entity in entities) Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity, "from": this.playerID, "to": 0 }); // Reveal the map for this player. cmpRangeManager.SetLosRevealAll(this.playerID, true); // Send a chat message notifying of the player's defeat. var notification = {"type": "defeat", "players": [this.playerID]}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); }; Player.prototype.OnDiplomacyChanged = function() { this.UpdateSharedLos(); }; Player.prototype.SetCheatsEnabled = function(flag) { this.cheatsEnabled = flag; }; Player.prototype.GetCheatsEnabled = function() { return this.cheatsEnabled; }; Player.prototype.SetCheatTimeMultiplier = function(time) { this.cheatTimeMultiplier = time; }; Player.prototype.GetCheatTimeMultiplier = function() { return this.cheatTimeMultiplier; }; Player.prototype.TributeResource = function(player, amounts) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(player), IID_Player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; if (!this.SubtractResourcesOrNotify(amounts)) return; cmpPlayer.AddResources(amounts); var total = Object.keys(amounts).reduce(function (sum, type){ return sum + amounts[type]; }, 0); var cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpOurStatisticsTracker) cmpOurStatisticsTracker.IncreaseTributesSentCounter(total); var cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpTheirStatisticsTracker) cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total); var notification = {"type": "tribute", "players": [player], "donator": this.playerID, "amounts": amounts}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification(notification); }; +Player.prototype.AddDisabledTemplate = function(template) +{ + this.disabledTemplates[template] = true; + Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); + var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({"type": "resetselectionpannel", "players": [this.GetPlayerID()]}); +}; + +Player.prototype.RemoveDisabledTemplate = function(template) +{ + this.disabledTemplates[template] = false; + Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); + var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({"type": "resetselectionpannel", "players": [this.GetPlayerID()]}); +}; + +Player.prototype.SetDisabledTemplates = function(templates) +{ + this.disabledTemplates = templates; + Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); + var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({"type": "resetselectionpannel", "players": [this.GetPlayerID()]}); +}; + +Player.prototype.GetDisabledTemplates = function(templates) +{ + return this.disabledTemplates; +}; + Engine.RegisterComponentType(IID_Player, "Player", Player); Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 15613) +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 15614) @@ -1,788 +1,803 @@ var g_ProgressInterval = 1000; const MAX_QUEUE_SIZE = 16; function ProductionQueue() {} ProductionQueue.prototype.Schema = "Allows the building to train new units and research technologies" + "" + "0.7" + "" + "\n units/{civ}_support_female_citizen\n units/{civ}_support_trader\n units/celt_infantry_spearman_b\n " + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + ""; ProductionQueue.prototype.Init = function() { this.nextID = 1; this.queue = []; // Queue items are: // { // "id": 1, // "player": 1, // who paid for this batch; we need this to cope with refunds cleanly // "unitTemplate": "units/example", // "count": 10, // "neededSlots": 3, // number of population slots missing for production to begin // "resources": { "wood": 100, ... }, // resources per unit, multiply by count to get total // "population": 1, // population per unit, multiply by count to get total // "productionStarted": false, // true iff we have reserved population // "timeTotal": 15000, // msecs // "timeRemaining": 10000, // msecs // } // // { // "id": 1, // "player": 1, // who paid for this research; we need this to cope with refunds cleanly // "technologyTemplate": "example_tech", // "resources": { "wood": 100, ... }, // resources needed for research // "productionStarted": false, // true iff production has started // "timeTotal": 15000, // msecs // "timeRemaining": 10000, // msecs // } this.timer = undefined; // g_ProgressInterval msec timer, active while the queue is non-empty this.paused = false; this.entityCache = []; this.spawnNotified = false; this.alertRaiser = undefined; }; ProductionQueue.prototype.PutUnderAlert = function(raiser) { this.alertRaiser = raiser; }; ProductionQueue.prototype.ResetAlert = function() { this.alertRaiser = undefined; }; /* * Returns list of entities that can be trained by this building. */ ProductionQueue.prototype.GetEntitiesList = function() { return this.entitiesList; }; ProductionQueue.prototype.CalculateEntitiesList = function() { this.entitiesList = []; if (!this.template.Entities) return; var string = this.template.Entities._string; if (!string) return; // Replace the "{civ}" codes with this entity's civ ID var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{civ\}/g, cmpIdentity.GetCiv()); var entitiesList = string.split(/\s+/); - + + // Remove disabled entities + var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player) + var disabledEntities = cmpPlayer.GetDisabledTemplates(); + + for (var i = entitiesList.length - 1; i >= 0; --i) + if (disabledEntities[entitiesList[i]]) + entitiesList.splice(i, 1); + // check if some templates need to show their advanced or elite version var upgradeTemplate = function(templateName) { var template = cmpTemplateManager.GetTemplate(templateName); while (template && template.Promotion !== undefined) { var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, playerID, template); if (requiredXp > 0) break; templateName = template.Promotion.Entity; template = cmpTemplateManager.GetTemplate(templateName); } return templateName; }; var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - var playerID = QueryOwnerInterface(this.entity, IID_Player).GetPlayerID(); + var playerID = cmpPlayer.GetPlayerID(); for each (var templateName in entitiesList) this.entitiesList.push(upgradeTemplate(templateName)); for each (var item in this.queue) { if (item.unitTemplate) item.unitTemplate = upgradeTemplate(item.unitTemplate); } }; /* * Returns list of technologies that can be researched by this building. */ ProductionQueue.prototype.GetTechnologiesList = function() { if (!this.template.Technologies) return []; var string = this.template.Technologies._string; if (!string) return []; var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (!cmpTechnologyManager) return []; var techs = string.split(/\s+/); var techList = []; var superseded = {}; // Stores the tech which supersedes the key // Add any top level technologies to an array which corresponds to the displayed icons // Also store what a technology is superceded by in the superceded object {"tech1":"techWhichSupercedesTech1", ...} for (var i in techs) { var tech = techs[i]; var template = cmpTechnologyManager.GetTechnologyTemplate(tech); if (!template.supersedes || techs.indexOf(template.supersedes) === -1) techList.push(tech); else superseded[template.supersedes] = tech; } // Now make researched/in progress techs invisible for (var i in techList) { var tech = techList[i]; while (this.IsTechnologyResearchedOrInProgress(tech)) { tech = superseded[tech]; } techList[i] = tech; } var ret = [] // This inserts the techs into the correct positions to line up the technology pairs for (var i = 0; i < techList.length; i++) { var tech = techList[i]; if (!tech) { ret[i] = undefined; continue; } var template = cmpTechnologyManager.GetTechnologyTemplate(tech); if (template.top) ret[i] = {"pair": true, "top": template.top, "bottom": template.bottom}; else ret[i] = tech; } return ret; }; ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech) { if (!tech) return false; var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); var template = cmpTechnologyManager.GetTechnologyTemplate(tech); if (template.top) { return (cmpTechnologyManager.IsTechnologyResearched(template.top) || cmpTechnologyManager.IsInProgress(template.top) || cmpTechnologyManager.IsTechnologyResearched(template.bottom) || cmpTechnologyManager.IsInProgress(template.bottom)) } else { return (cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech)) } }; /* * Adds a new batch of identical units to train or a technology to research to the production queue. */ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadata) { // TODO: there should probably be a limit on the number of queued batches // TODO: there should be a way for the GUI to determine whether it's going // to be possible to add a batch (based on resource costs and length limits) var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (this.queue.length < MAX_QUEUE_SIZE) { if (type == "unit") { // Find the template data so we can determine the build costs var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.GetTemplate(templateName); if (!template) return; if (template.Promotion) { var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template); if (requiredXp == 0) { this.AddBatch(template.Promotion.Entity, type, count, metadata); return; } } // Apply a time discount to larger batches. var timeMult = this.GetBatchTime(count); // We need the costs after tech modifications // Obviously we don't have the entities yet, so we must use template data var costs = {}; var totalCosts = {}; var buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, cmpPlayer.GetPlayerID(), template); var time = timeMult * buildTime; for (var r in template.Cost.Resources) { costs[r] = ApplyValueModificationsToTemplate("Cost/Resources/"+r, +template.Cost.Resources[r], cmpPlayer.GetPlayerID(), template); totalCosts[r] = Math.floor(count * costs[r]); } var population = +template.Cost.Population; // TrySubtractResources should report error to player (they ran out of resources) if (!cmpPlayer.TrySubtractResources(totalCosts)) return; // Update entity count in the EntityLimits component if (template.TrainingRestrictions) { var unitCategory = template.TrainingRestrictions.Category; var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory, count); } this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "resources": costs, "population": population, "productionStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); // Call the related trigger event var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("TrainingQueued", {"playerid": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "trainerEntity": this.entity}); } else if (type == "technology") { // Load the technology template var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager); var template = cmpTechTempMan.GetTemplate(templateName); if (!template) return; var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); var time = template.researchTime * cmpPlayer.GetCheatTimeMultiplier(); var cost = {}; for each (var r in ["food", "wood", "stone", "metal"]) cost[r] = Math.floor(template.cost[r]); // TrySubtractResources should report error to player (they ran out of resources) if (!cmpPlayer.TrySubtractResources(cost)) return; // Tell the technology manager that we have started researching this so that people can't research the same // thing twice. var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.QueuedResearch(templateName, this.entity); if (this.queue.length == 0) cmpTechnologyManager.StartedResearch(templateName); this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "count": 1, "technologyTemplate": templateName, "resources": deepcopy(template.cost), // need to copy to avoid serialization problems "productionStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); // Call the related trigger event var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("ResearchQueued", {"playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity}); } else { warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue"); return; } Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); // If this is the first item in the queue, start the timer if (!this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {}); } } else { var notification = {"players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("The production queue is full."), "translateMessage": true }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } }; /* * Removes an existing batch of units from the production queue. * Refunds resource costs and population reservations. */ ProductionQueue.prototype.RemoveBatch = function(id) { // Destroy any cached entities (those which didn't spawn for some reason) for (var i = 0; i < this.entityCache.length; ++i) { Engine.DestroyEntity(this.entityCache[i]); } this.entityCache = []; for (var i = 0; i < this.queue.length; ++i) { var item = this.queue[i]; if (item.id != id) continue; // Now we've found the item to remove var cmpPlayer = QueryPlayerIDInterface(item.player, IID_Player); // Update entity count in the EntityLimits component if (item.unitTemplate) { var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.GetTemplate(item.unitTemplate); if (template.TrainingRestrictions) { var unitCategory = template.TrainingRestrictions.Category; var cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory, -item.count); } } // Refund the resource cost for this batch var totalCosts = {}; var cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker); for each (var r in ["food", "wood", "stone", "metal"]) { totalCosts[r] = Math.floor(item.count * item.resources[r]); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -totalCosts[r]); } cmpPlayer.AddResources(totalCosts); // Remove reserved population slots if necessary if (item.productionStarted && item.unitTemplate) cmpPlayer.UnReservePopulationSlots(item.population * item.count); // Mark the research as stopped if we cancel it if (item.technologyTemplate) { // item.player is used as this.entity's owner may be invalid (deletion, etc.) var cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager); cmpTechnologyManager.StoppedResearch(item.technologyTemplate); } // Remove from the queue // (We don't need to remove the timer - it'll expire if it discovers the queue is empty) this.queue.splice(i, 1); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); return; } }; /* * Returns basic data from all batches in the production queue. */ ProductionQueue.prototype.GetQueue = function() { var out = []; for each (var item in this.queue) { out.push({ "id": item.id, "unitTemplate": item.unitTemplate, "technologyTemplate": item.technologyTemplate, "count": item.count, "neededSlots": item.neededSlots, "progress": 1 - ( item.timeRemaining / (item.timeTotal || 1) ), "timeRemaining": item.timeRemaining, "metadata": item.metadata, }); } return out; }; /* * Removes all existing batches from the queue. */ ProductionQueue.prototype.ResetQueue = function() { // Empty the production queue and refund all the resource costs // to the player. (This is to avoid players having to micromanage their // buildings' queues when they're about to be destroyed or captured.) while (this.queue.length) this.RemoveBatch(this.queue[0].id); }; /* * Returns batch build time. */ ProductionQueue.prototype.GetBatchTime = function(batchSize) { var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); var batchTimeModifier = ApplyValueModificationsToEntity("ProductionQueue/BatchTimeModifier", +this.template.BatchTimeModifier, this.entity); // TODO: work out what equation we should use here. return Math.pow(batchSize, batchTimeModifier) * cmpPlayer.GetCheatTimeMultiplier(); }; ProductionQueue.prototype.OnOwnershipChanged = function(msg) { if (msg.from != -1) { // Unset flag that previous owner's training may be blocked var cmpPlayer = QueryPlayerIDInterface(msg.from, IID_Player); if (cmpPlayer && this.queue.length > 0) cmpPlayer.UnBlockTraining(); } if (msg.to != -1) this.CalculateEntitiesList(); // Reset the production queue whenever the owner changes. // (This should prevent players getting surprised when they capture // an enemy building, and then loads of the enemy's civ's soldiers get // created from it. Also it means we don't have to worry about // updating the reserved pop slots.) this.ResetQueue(); }; ProductionQueue.prototype.OnDestroy = function() { // Reset the queue to refund any resources this.ResetQueue(); if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /* * This function creates the entities and places them in world if possible * and returns the number of successfully created entities. * (some of these entities may be garrisoned directly if autogarrison, the others are spawned). */ ProductionQueue.prototype.SpawnUnits = function(templateName, count, metadata) { var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); var createdEnts = []; var spawnedEnts = []; if (this.entityCache.length == 0) { // We need entities to test spawning, but we don't want to waste resources, // so only create them once and use as needed for (var i = 0; i < count; ++i) { var ent = Engine.AddEntity(templateName); this.entityCache.push(ent); // Decrement entity count in the EntityLimits component // since it will be increased by EntityLimits.OnGlobalOwnershipChanged function, // i.e. we replace a 'trained' entity to an 'alive' one var cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions); if (cmpTrainingRestrictions) { var unitCategory = cmpTrainingRestrictions.GetCategory(); var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory,-1); } } } var cmpAutoGarrison = undefined; if (cmpRallyPoint) { var data = cmpRallyPoint.GetData()[0]; if (data && data.target && data.target == this.entity && data.command == "garrison") cmpAutoGarrison = Engine.QueryInterface(this.entity, IID_GarrisonHolder); } for (var i = 0; i < count; ++i) { var ent = this.entityCache[0]; var cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); if (cmpAutoGarrison && cmpAutoGarrison.PerformGarrison(ent)) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.Autogarrison(this.entity); } else { var pos = cmpFootprint.PickSpawnPoint(ent); if (pos.y < 0) { // Fail: there wasn't any space to spawn the unit break; } else { // Successfully spawned var cmpNewPosition = Engine.QueryInterface(ent, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); // TODO: what direction should they face in? spawnedEnts.push(ent); } } var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent); // Play a sound, but only for the first in the batch (to avoid nasty phasing effects) if (createdEnts.length == 0) PlaySound("trained", ent); this.entityCache.shift(); createdEnts.push(ent); } if (spawnedEnts.length > 0 && !cmpAutoGarrison) { // If a rally point is set, walk towards it (in formation) using a suitable command based on where the // rally point is placed. if (cmpRallyPoint) { var rallyPos = cmpRallyPoint.GetPositions()[0]; if (rallyPos) { var commands = GetRallyPointCommands(cmpRallyPoint, spawnedEnts); for each(var com in commands) ProcessCommand(cmpOwnership.GetOwner(), com); } } } if (createdEnts.length > 0) { Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": createdEnts, "owner": cmpOwnership.GetOwner(), "metadata": metadata, }); if(this.alertRaiser && spawnedEnts.length > 0) { var cmpAlertRaiser = Engine.QueryInterface(this.alertRaiser, IID_AlertRaiser); if(cmpAlertRaiser) cmpAlertRaiser.UpdateUnits(spawnedEnts); } } return createdEnts.length; }; /* * Increments progress on the first batch in the production queue, and blocks the * queue if population limit is reached or some units failed to spawn. */ ProductionQueue.prototype.ProgressTimeout = function(data) { // Check if the production is paused (eg the entity is garrisoned) if (this.paused) return; // Allocate the 1000msecs to as many queue items as it takes // until we've used up all the time (so that we work accurately // with items that take fractions of a second) var time = g_ProgressInterval; var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); while (time > 0 && this.queue.length) { var item = this.queue[0]; if (!item.productionStarted) { // If the item is a unit then do population checks if (item.unitTemplate) { // Batch's training hasn't started yet. // Try to reserve the necessary population slots item.neededSlots = cmpPlayer.TryReservePopulationSlots(item.population * item.count); if (item.neededSlots) { // Not enough slots available - don't train this batch now // (we'll try again on the next timeout) // Set flag that training is blocked cmpPlayer.BlockTraining(); break; } // Unset flag that training is blocked cmpPlayer.UnBlockTraining(); } if (item.technologyTemplate) { // Mark the research as started. var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.StartedResearch(item.technologyTemplate); } item.productionStarted = true; if (item.unitTemplate) Engine.PostMessage(this.entity, MT_TrainingStarted, {"entity": this.entity}); } // If we won't finish the batch now, just update its timer if (item.timeRemaining > time) { item.timeRemaining -= time; // send a message for the AIs. Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); break; } if (item.unitTemplate) { var numSpawned = this.SpawnUnits(item.unitTemplate, item.count, item.metadata); if (numSpawned == item.count) { // All entities spawned, this batch finished cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); time -= item.timeRemaining; this.queue.shift(); // Unset flag that training is blocked cmpPlayer.UnBlockTraining(); this.spawnNotified = false; Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } else { if (numSpawned > 0) { // Only partially finished cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); item.count -= numSpawned; Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } // Some entities failed to spawn // Set flag that training is blocked cmpPlayer.BlockTraining(); if (!this.spawnNotified) { var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Can't find free space to spawn trained units" }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); this.spawnNotified = true; } break; } } else if (item.technologyTemplate) { var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.ResearchTechnology(item.technologyTemplate); var template = cmpTechnologyManager.GetTechnologyTemplate(item.technologyTemplate); if (template && template.soundComplete) { var cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); if (cmpSoundManager) cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity); } time -= item.timeRemaining; this.queue.shift(); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } } // If the queue's empty, delete the timer, else repeat it if (this.queue.length == 0) { this.timer = undefined; // Unset flag that training is blocked // (This might happen when the player unqueues all batches) cmpPlayer.UnBlockTraining(); } else { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, data); } }; ProductionQueue.prototype.PauseProduction = function() { this.timer = undefined; this.paused = true; }; ProductionQueue.prototype.UnpauseProduction = function() { this.paused = false; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {}); }; ProductionQueue.prototype.OnValueModification = function(msg) { // if the promotion requirements of units is changed, // update the entities list so that automatically promoted units are shown // appropriately in the list if (msg.component == "Promotion") this.CalculateEntitiesList(); }; +ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg) +{ + // if the disabled templates of the player is changed, + // update the entities list so that this is reflected there + this.CalculateEntitiesList(); +}; + Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);