Index: ps/trunk/binaries/data/mods/public/gui/session/diplomacy/playercontrols/StanceButton.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/diplomacy/playercontrols/StanceButton.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/gui/session/diplomacy/playercontrols/StanceButton.js (revision 27722) @@ -1,60 +1,60 @@ /** * This class provides one button per diplomatic stance for a given player. */ DiplomacyDialogPlayerControl.prototype.StanceButtonManager = class { constructor(playerID) { this.buttons = this.Stances.map(stance => new this.StanceButton(playerID, stance)); } update(playerInactive) { - let hidden = playerInactive || GetSimState().ceasefireActive || g_Players[g_ViewedPlayer].teamsLocked; + let hidden = playerInactive || GetSimState().ceasefireActive || g_Players[g_ViewedPlayer].teamLocked; for (let button of this.buttons) button.update(hidden); } }; DiplomacyDialogPlayerControl.prototype.StanceButtonManager.prototype.Stances = ["Ally", "Neutral", "Enemy"]; /** * This class manages a button that if pressed, will change the diplomatic stance to the given player to the given stance. */ DiplomacyDialogPlayerControl.prototype.StanceButtonManager.prototype.StanceButton = class { constructor(playerID, stance) { this.playerID = playerID; this.stance = stance; this.button = Engine.GetGUIObjectByName("diplomacyPlayer" + stance + "[" + (playerID - 1) + "]"); this.button.onPress = this.onPress.bind(this); } update(hidden) { this.button.hidden = hidden; if (hidden) return; let isCurrentStance = g_Players[g_ViewedPlayer]["is" + this.stance][this.playerID]; this.button.enabled = !isCurrentStance && controlsPlayer(g_ViewedPlayer); this.button.caption = isCurrentStance ? translateWithContext("diplomatic stance selection", this.StanceSelection) : ""; } onPress() { Engine.PostNetworkCommand({ "type": "diplomacy", "player": this.playerID, "to": this.stance.toLowerCase() }); } }; DiplomacyDialogPlayerControl.prototype.StanceButtonManager.prototype.StanceButton.prototype.StanceSelection = markForTranslationWithContext("diplomatic stance selection", "x"); Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js (revision 27722) @@ -1,15 +1,15 @@ /** * This class reports the chosen settings and victory state of the participating players. */ LobbyRatingReport.prototype.Players = class { insertValues(report, playerStates) { Object.assign(report, { "playerStates": playerStates.map(playerState => playerState.state).join(",") + ",", "civs": playerStates.map(playerState => playerState.civ).join(",") + ",", "teams": playerStates.map(playerState => playerState.team).join(",") + ",", - "teamsLocked": String(playerStates.every(playerState => playerState.teamsLocked)) + "teamsLocked": String(playerStates.every(playerState => playerState.teamLocked)) }); } }; Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 27722) @@ -1,881 +1,881 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_CivData = loadCivData(false, true); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; var g_Ambient; var g_AutoFormation; var g_Chat; var g_Cheats; var g_DeveloperOverlay; var g_DiplomacyColors; var g_DiplomacyDialog; var g_GameSpeedControl; var g_Menu; var g_MiniMapPanel; var g_NetworkStatusOverlay; var g_NetworkDelayOverlay; var g_ObjectivesDialog; var g_OutOfSyncNetwork; var g_OutOfSyncReplay; var g_PanelEntityManager; var g_PauseControl; var g_PauseOverlay; var g_PlayerViewControl; var g_QuitConfirmationDefeat; var g_QuitConfirmationReplay; var g_RangeOverlayManager; var g_ResearchProgress; var g_TimeNotificationOverlay; var g_TopPanel; var g_TradeDialog; /** * Map, player and match settings set in game setup. */ const g_InitAttributes = deepfreeze(Engine.GuiInterfaceCall("GetInitAttributes")); /** * True if this is a multiplayer game. */ const g_IsNetworked = Engine.HasNetClient(); /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ var g_IsController = !g_IsNetworked || Engine.IsNetController(); /** * Whether we have finished the synchronization and * can start showing simulation related message boxes. */ var g_IsNetworkedActive = false; /** * True if the connection to the server has been lost. */ var g_Disconnected = false; /** * True if the current user has observer capabilities. */ var g_IsObserver = false; /** * True if the current user has rejoined (or joined the game after it started). */ var g_HasRejoined = false; /** * The playerID selected in the change perspective tool. */ var g_ViewedPlayer = Engine.GetPlayerID(); /** * True if the camera should focus on attacks and player commands * and select the affected units. */ var g_FollowPlayer = false; /** * Cache the basic player data (name, civ, color). */ var g_Players = []; /** * Last time when onTick was called(). * Used for animating the main menu. */ var g_LastTickTime = Date.now(); /** * Recalculate which units have their status bars shown with this frequency in milliseconds. */ var g_StatusBarUpdate = 200; /** * For restoring selection, order and filters when returning to the replay menu */ var g_ReplaySelectionData; /** * Remembers which clients are assigned to which player slots. * The keys are GUIDs or "local" in single-player. */ var g_PlayerAssignments; /** * Whether the entire UI should be hidden (useful for promotional screenshots). * Can be toggled with a hotkey. */ var g_ShowGUI = true; /** * Whether status bars should be shown for all of the player's units. */ var g_ShowAllStatusBars = false; /** * Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update). */ var g_SimState; var g_EntityStates = {}; var g_TemplateData = {}; var g_TechnologyData = {}; var g_ResourceData = new Resources(); /** * These handlers are called each time a new turn was simulated. * Use this as sparely as possible. */ var g_SimulationUpdateHandlers = new Set(); /** * These handlers are called after the player states have been initialized. */ var g_PlayersInitHandlers = new Set(); /** * These handlers are called when a player has been defeated or won the game. */ var g_PlayerFinishedHandlers = new Set(); /** * These events are fired whenever the player added or removed entities from the selection. */ var g_EntitySelectionChangeHandlers = new Set(); /** * These events are fired when the user has performed a hotkey assignment change. * Currently only fired on init, but to be fired from any hotkey editor dialog. */ var g_HotkeyChangeHandlers = new Set(); /** * List of additional entities to highlight. */ var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; /** * Order in which the panel entities are shown. */ var g_PanelEntityOrder = ["Hero", "Relic"]; /** * Unit classes to be checked for the idle-worker-hotkey. */ var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "Citizen"]; /** * Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey. */ var g_MilitaryTypes = ["Melee", "Ranged"]; function GetSimState() { if (!g_SimState) g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState")); return g_SimState; } function GetMultipleEntityStates(ents) { if (!ents.length) return null; let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents); for (let item of entityStates) g_EntityStates[item.entId] = item.state && deepfreeze(item.state); return entityStates; } function GetEntityState(entId) { if (!g_EntityStates[entId]) { let entityState = Engine.GuiInterfaceCall("GetEntityState", entId); g_EntityStates[entId] = entityState && deepfreeze(entityState); } return g_EntityStates[entId]; } /** * Returns template data calling GetTemplateData defined in GuiInterface.js * and deepfreezing returned object. * @param {string} templateName - Data of this template will be returned. * @param {number|undefined} player - Modifications of this player will be applied to the template. * If undefined, id of player calling this method will be used. */ function GetTemplateData(templateName, player) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": player }); translateObjectKeys(template, ["specific", "generic", "tooltip"]); g_TemplateData[templateName] = deepfreeze(template); } return g_TemplateData[templateName]; } function GetTechnologyData(technologyName, civ) { if (!g_TechnologyData[civ]) g_TechnologyData[civ] = {}; if (!(technologyName in g_TechnologyData[civ])) { const tech = TechnologyTemplates.Get(technologyName); if (!tech) return; let template = GetTechnologyDataHelper(tech, civ, g_ResourceData); translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); g_TechnologyData[civ][technologyName] = deepfreeze(template); } return g_TechnologyData[civ][technologyName]; } function init(initData, hotloadData) { if (!g_Settings) { Engine.EndGame(); Engine.SwitchGuiPage("page_pregame.xml"); return; } // Fallback used by atlas g_PlayerAssignments = initData ? initData.playerAssignments : { "local": { "player": 1 } }; // Fallback used by atlas and autostart games if (g_PlayerAssignments.local && !g_PlayerAssignments.local.name) g_PlayerAssignments.local.name = singleplayerName(); if (initData) { g_ReplaySelectionData = initData.replaySelectionData; g_HasRejoined = initData.isRejoining; if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); } if (g_InitAttributes.campaignData) g_CampaignSession = new CampaignSession(g_InitAttributes.campaignData); let mapCache = new MapCache(); g_Cheats = new Cheats(); g_DiplomacyColors = new DiplomacyColors(); g_PlayerViewControl = new PlayerViewControl(); g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects); g_PauseControl = new PauseControl(); g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay); g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates); g_Ambient = new Ambient(); g_AutoFormation = new AutoFormation(); g_Chat = new Chat(g_PlayerViewControl, g_Cheats); g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection); g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors); g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl); g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat); g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes); g_NetworkStatusOverlay = new NetworkStatusOverlay(); g_NetworkDelayOverlay = new NetworkDelayOverlay(); g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl, mapCache); g_OutOfSyncNetwork = new OutOfSyncNetwork(); g_OutOfSyncReplay = new OutOfSyncReplay(); g_PanelEntityManager = new PanelEntityManager(g_PlayerViewControl, g_Selection, g_PanelEntityOrder); g_PauseOverlay = new PauseOverlay(g_PauseControl); g_QuitConfirmationDefeat = new QuitConfirmationDefeat(); g_QuitConfirmationReplay = new QuitConfirmationReplay(); g_RangeOverlayManager = new RangeOverlayManager(g_Selection); g_ResearchProgress = new ResearchProgress(g_PlayerViewControl, g_Selection); g_TradeDialog = new TradeDialog(g_PlayerViewControl); g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl); g_TimeNotificationOverlay = new TimeNotificationOverlay(g_PlayerViewControl); initBatchTrain(); initDisplayedNames(); initSelectionPanels(); LoadModificationTemplates(); updatePlayerData(); initializeMusic(); // before changing the perspective Engine.SetBoundingBoxDebugOverlay(false); for (let handler of g_PlayersInitHandlers) handler(); for (let handler of g_HotkeyChangeHandlers) handler(); if (hotloadData) { g_Selection.selected = hotloadData.selection; g_PlayerAssignments = hotloadData.playerAssignments; g_Players = hotloadData.player; } // TODO: use event instead onSimulationUpdate(); setTimeout(displayGamestateNotifications, 1000); } function registerPlayersInitHandler(handler) { g_PlayersInitHandlers.add(handler); } function registerPlayersFinishedHandler(handler) { g_PlayerFinishedHandlers.add(handler); } function registerSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.add(handler); } function unregisterSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.delete(handler); } function registerEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.add(handler); } function unregisterEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.delete(handler); } function registerHotkeyChangeHandler(handler) { g_HotkeyChangeHandlers.add(handler); } function updatePlayerData() { let simState = GetSimState(); if (!simState) return; let playerData = []; for (let i = 0; i < simState.players.length; ++i) { let playerState = simState.players[i]; playerData.push({ "name": playerState.name, "civ": playerState.civ, "color": { "r": playerState.color.r * 255, "g": playerState.color.g * 255, "b": playerState.color.b * 255, "a": playerState.color.a * 255 }, "team": playerState.team, - "teamsLocked": playerState.teamsLocked, + "teamLocked": playerState.teamLocked, "cheatsEnabled": playerState.cheatsEnabled, "state": playerState.state, "isAlly": playerState.isAlly, "isMutualAlly": playerState.isMutualAlly, "isNeutral": playerState.isNeutral, "isEnemy": playerState.isEnemy, "guid": undefined, // network guid for players controlled by hosts "offline": g_Players[i] && !!g_Players[i].offline }); } for (let guid in g_PlayerAssignments) { let playerID = g_PlayerAssignments[guid].player; if (!playerData[playerID]) continue; playerData[playerID].guid = guid; playerData[playerID].name = g_PlayerAssignments[guid].name; } g_Players = playerData; } /** * @param {number} ent - The entity to get its ID for. * @return {number} - The entity ID of the entity or of its garrisonHolder. */ function getEntityOrHolder(ent) { let entState = GetEntityState(ent); if (entState && !entState.position && entState.garrisonable && entState.garrisonable.holder != INVALID_ENTITY) return getEntityOrHolder(entState.garrisonable.holder); return ent; } function initializeMusic() { initMusic(); if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music) global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music); global.music.setState(global.music.states.PEACE); } function resetTemplates() { // Update GUI and clear player-dependent cache g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); // TODO: do this more selectively onSimulationUpdate(); } /** * Returns true if the player with that ID is in observermode. */ function isPlayerObserver(playerID) { let playerStates = GetSimState().players; return !playerStates[playerID] || playerStates[playerID].state != "active"; } /** * Returns true if the current user can issue commands for that player. */ function controlsPlayer(playerID) { let playerStates = GetSimState().players; return !!playerStates[Engine.GetPlayerID()] && playerStates[Engine.GetPlayerID()].controlsAll || Engine.GetPlayerID() == playerID && !!playerStates[playerID] && playerStates[playerID].state != "defeated"; } /** * Called when one or more players have won or were defeated. * * @param {array} - IDs of the players who have won or were defeated. * @param {Object} - a plural string stating the victory reason. * @param {boolean} - whether these players have won or lost. */ function playersFinished(players, victoryString, won) { addChatMessage({ "type": "playerstate", "message": victoryString, "players": players }); updatePlayerData(); // TODO: The other calls in this function should move too for (let handler of g_PlayerFinishedHandlers) handler(players, won); if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning()) return; global.music.setState( won ? global.music.states.VICTORY : global.music.states.DEFEAT ); } function resumeGame() { g_PauseControl.implicitResume(); } function closeOpenDialogs() { g_Menu.close(); g_Chat.closePage(); g_DiplomacyDialog.close(); g_ObjectivesDialog.close(); g_TradeDialog.close(); } function endGame(showSummary) { // Before ending the game let replayDirectory = Engine.GetCurrentReplayDirectory(); let simData = Engine.GuiInterfaceCall("GetReplayMetadata"); let playerID = Engine.GetPlayerID(); Engine.EndGame(); // After the replay file was closed in EndGame // Done here to keep EndGame small if (!g_IsReplay) Engine.AddReplayToCache(replayDirectory); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); let summaryData = { "sim": simData, "gui": { "dialog": false, "assignedPlayer": playerID, "disconnected": g_Disconnected, "isReplay": g_IsReplay, "replayDirectory": !g_HasRejoined && replayDirectory, "replaySelectionData": g_ReplaySelectionData } }; if (g_InitAttributes.campaignData) { let menu = g_CampaignSession.getMenu(); if (g_InitAttributes.campaignData.skipSummary) { Engine.SwitchGuiPage(menu); return; } summaryData.campaignData = { "filename": g_InitAttributes.campaignData.run }; summaryData.nextPage = menu; } if (showSummary) Engine.SwitchGuiPage("page_summary.xml", summaryData); else if (g_InitAttributes.campaignData) Engine.SwitchGuiPage(summaryData.nextPage, summaryData.campaignData); else if (Engine.HasXmppClient()) Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false }); else if (g_IsReplay) Engine.SwitchGuiPage("page_replaymenu.xml"); else Engine.SwitchGuiPage("page_pregame.xml"); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { "selection": g_Selection.selected, "playerAssignments": g_PlayerAssignments, "player": g_Players, }; } function getSavedGameData() { return { "groups": g_Groups.groups }; } function restoreSavedGameData(data) { // Restore camera if any if (data.camera) Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ, data.camera.RotX, data.camera.RotY, data.camera.Zoom); // Clear selection when loading a game g_Selection.reset(); // Restore control groups for (let groupNumber in data.groups) { g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups; g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents; } updateGroups(); } /** * Called every frame. */ function onTick() { if (!g_Settings) return; let now = Date.now(); let tickLength = now - g_LastTickTime; g_LastTickTime = now; handleNetMessages(); updateCursorAndTooltip(); if (g_Selection.dirty) { g_Selection.dirty = false; // When selection changed, get the entityStates of new entities GetMultipleEntityStates(g_Selection.filter(entId => !g_EntityStates[entId])); for (let handler of g_EntitySelectionChangeHandlers) handler(); updateGUIObjects(); // Display rally points for selected structures. if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength) recalculateStatusBarDisplay(); updateTimers(); Engine.GuiInterfaceCall("ClearRenamedEntities"); let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; if (isPlayingCinemaPath) updateCinemaOverlay(); } function onSimulationUpdate() { // Templates change depending on technologies and auras, so they have to be reloaded after such a change. // g_TechnologyData data never changes, so it shouldn't be deleted. g_EntityStates = {}; if (Engine.GuiInterfaceCall("IsTemplateModified")) { g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); } g_SimState = undefined; // Some changes may require re-rendering the selection. if (Engine.GuiInterfaceCall("IsSelectionDirty")) { g_Selection.onChange(); Engine.GuiInterfaceCall("ResetSelectionDirty"); } if (!GetSimState()) return; GetMultipleEntityStates(g_Selection.toList()); for (let handler of g_SimulationUpdateHandlers) handler(); // TODO: Move to handlers updateCinemaPath(); handleNotifications(); updateGUIObjects(); } function toggleGUI() { g_ShowGUI = !g_ShowGUI; updateCinemaPath(); } function updateCinemaPath() { let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath; Engine.GetGUIObjectByName("cinemaOverlay").hidden = !isPlayingCinemaPath; Engine.ConfigDB_CreateValue("user", "silhouettes", !isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true" ? "true" : "false"); } function updateCinemaOverlay() { let cinemaOverlay = Engine.GetGUIObjectByName("cinemaOverlay"); let width = cinemaOverlay.getComputedSize().right; let height = cinemaOverlay.getComputedSize().bottom; let barHeight = (height - width / 2.39) / 2; if (barHeight < 0) barHeight = 0; let cinemaBarTop = Engine.GetGUIObjectByName("cinemaBarTop"); let cinemaBarTopSize = cinemaBarTop.size; cinemaBarTopSize.bottom = barHeight; cinemaBarTop.size = cinemaBarTopSize; let cinemaBarBottom = Engine.GetGUIObjectByName("cinemaBarBottom"); let cinemaBarBottomSize = cinemaBarBottom.size; cinemaBarBottomSize.top = -barHeight; cinemaBarBottom.size = cinemaBarBottomSize; } // TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updateGroups(); updateSelectionDetails(); updateBuildingPlacementPreview(); if (!g_IsObserver) { // Update music state on basis of battle state. let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer); if (battleState) global.music.setState(global.music.states[battleState]); } } function updateGroups() { g_Groups.update(); // Determine the sum of the costs of a given template let getCostSum = (ent) => { let cost = GetTemplateData(GetEntityState(ent).template).cost; return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0; }; for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children) { Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = +i + 1; let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]"); button.hidden = g_Groups.groups[i].getTotalCount() == 0; button.onPress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i); button.onDoublePress = (function(i) { return function() { performGroup("snap", i); }; })(i); button.onPressRight = (function(i) { return function() { performGroup("breakUp", i); }; })(i); // Choose the icon of the most common template (or the most costly if it's not unique) if (g_Groups.groups[i].getTotalCount() > 0) { let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => { if (pre.ents.length == cur.ents.length) return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur; return pre.ents.length > cur.ents.length ? pre : cur; }).ents[0]).template).icon; Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite = icon ? ("stretched:session/portraits/" + icon) : "groupsIcon"; } setPanelObjectPosition(button, i, 1); } } /** * Toggles the display of status bars for all of the player's entities. * * @param {boolean} remove - Whether to hide all previously shown status bars. */ function recalculateStatusBarDisplay(remove = false) { let entities; if (g_ShowAllStatusBars && !remove) entities = g_ViewedPlayer == -1 ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer); else { let selected = g_Selection.toList(); for (const ent of g_Selection.highlighted) selected.push(ent); // Remove selected entities from the 'all entities' array, // to avoid disabling their status bars. entities = Engine.GuiInterfaceCall( g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", { "viewedPlayer": g_ViewedPlayer }).filter(idx => selected.indexOf(idx) == -1); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars && !remove, "showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true", "showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true" }); } function removeStatusBarDisplay() { if (g_ShowAllStatusBars) recalculateStatusBarDisplay(true); } /** * Updates the primary/secondary names in the simulation and GUI. */ function updateDisplayedNames() { g_SpecificNamesPrimary = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 2; g_ShowSecondaryNames = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 1; } /** * Inverts the given configuration boolean and returns the current state. * For example "silhouettes". */ function toggleConfigBool(configName) { let enabled = Engine.ConfigDB_GetValue("user", configName) != "true"; Engine.ConfigDB_CreateAndSaveValue("user", configName, String(enabled)); return enabled; } // Update the additional list of entities to be highlighted. function updateAdditionalHighlight() { let entsAdd = []; // list of entities units to be highlighted let entsRemove = []; let highlighted = g_Selection.toList(); for (const ent of g_Selection.highlighted) highlighted.push(ent); if (g_ShowGuarding) // flag the guarding entities to add in this additional highlight for (const sel of g_Selection.toList()) { const state = GetEntityState(sel); if (!state.guard || !state.guard.entities.length) continue; for (let ent of state.guard.entities) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } if (g_ShowGuarded) // flag the guarded entities to add in this additional highlight for (const sel of g_Selection.toList()) { const state = GetEntityState(sel); if (!state.unitAI || !state.unitAI.isGuarding) continue; let ent = state.unitAI.isGuarding; if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } // flag the entities to remove (from the previously added) from this additional highlight for (let ent of g_AdditionalHighlight) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1) entsRemove.push(ent); _setHighlight(entsAdd, g_HighlightedAlpha, true); _setHighlight(entsRemove, 0, false); g_AdditionalHighlight = entsAdd; } Index: ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 27722) @@ -1,195 +1,195 @@ Trigger.prototype.InitCaptureTheRelic = function() { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let catafalqueTemplates = shuffleArray(cmpTemplateManager.FindAllTemplates(false).filter( name => GetIdentityClasses(cmpTemplateManager.GetTemplate(name).Identity || {}).indexOf("Relic") != -1)); let potentialSpawnPoints = TriggerHelper.GetLandSpawnPoints(); if (!potentialSpawnPoints.length) { error("No gaia entities found on this map that could be used as spawn points!"); return; } let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); let numSpawnedRelics = cmpEndGameManager.GetGameSettings().relicCount; this.playerRelicsCount = new Array(TriggerHelper.GetNumberOfPlayers()).fill(0, 1); this.playerRelicsCount[0] = numSpawnedRelics; for (let i = 0; i < numSpawnedRelics; ++i) { this.relics[i] = TriggerHelper.SpawnUnits(pickRandom(potentialSpawnPoints), catafalqueTemplates[i], 1, 0)[0]; let cmpPositionRelic = Engine.QueryInterface(this.relics[i], IID_Position); cmpPositionRelic.SetYRotation(randomAngle()); } }; Trigger.prototype.CheckCaptureTheRelicVictory = function(data) { let cmpIdentity = Engine.QueryInterface(data.entity, IID_Identity); if (!cmpIdentity || !cmpIdentity.HasClass("Relic") || data.from == INVALID_PLAYER) return; --this.playerRelicsCount[data.from]; if (data.to == -1) { warn("Relic entity " + data.entity + " has been destroyed."); this.relics.splice(this.relics.indexOf(data.entity), 1); } else ++this.playerRelicsCount[data.to]; this.DeleteCaptureTheRelicVictoryMessages(); this.CheckCaptureTheRelicCountdown(); }; /** * Check if a group of mutually allied players have acquired all relics. * The winning players are the relic owners and all players mutually allied to all relic owners. * Reset the countdown if the group of winning players changes or extends. */ Trigger.prototype.CheckCaptureTheRelicCountdown = function() { if (this.playerRelicsCount[0]) { this.DeleteCaptureTheRelicVictoryMessages(); return; } let activePlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers(); let relicOwners = activePlayers.filter(playerID => this.playerRelicsCount[playerID]); if (!relicOwners.length) { this.DeleteCaptureTheRelicVictoryMessages(); return; } let winningPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).GetAlliedVictory() ? - activePlayers.filter(playerID => relicOwners.every(owner => QueryPlayerIDInterface(playerID).IsMutualAlly(owner))) : + activePlayers.filter(playerID => relicOwners.every(owner => QueryPlayerIDInterface(playerID, IID_Diplomacy).IsMutualAlly(owner))) : [relicOwners[0]]; // All relicOwners should be mutually allied if (relicOwners.some(owner => winningPlayers.indexOf(owner) == -1)) { this.DeleteCaptureTheRelicVictoryMessages(); return; } // Reset the timer when playerAndAllies isn't the same as this.relicsVictoryCountdownPlayers if (winningPlayers.length != this.relicsVictoryCountdownPlayers.length || winningPlayers.some(player => this.relicsVictoryCountdownPlayers.indexOf(player) == -1)) { this.relicsVictoryCountdownPlayers = winningPlayers; this.StartCaptureTheRelicCountdown(winningPlayers); } }; Trigger.prototype.DeleteCaptureTheRelicVictoryMessages = function() { if (!this.relicsVictoryTimer) return; Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.relicsVictoryTimer); this.relicsVictoryTimer = undefined; let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage); cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage); this.relicsVictoryCountdownPlayers = []; }; Trigger.prototype.StartCaptureTheRelicCountdown = function(winningPlayers) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (this.relicsVictoryTimer) { cmpTimer.CancelTimer(this.relicsVictoryTimer); cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage); cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage); } if (!this.relics.length) return; let others = [-1]; for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer.GetState() == "won") return; if (winningPlayers.indexOf(playerID) == -1) others.push(playerID); } let cmpPlayer = QueryOwnerInterface(this.relics[0], IID_Player); if (!cmpPlayer) { warn("Relic entity " + this.relics[0] + " has no owner."); this.relics.splice(0, 1); this.CheckCaptureTheRelicCountdown(); return; } let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); let captureTheRelicDuration = cmpEndGameManager.GetGameSettings().relicDuration; let isTeam = winningPlayers.length > 1; this.ownRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({ "message": isTeam ? markForTranslation("%(_player_)s and their allies have captured all relics and will win in %(time)s.") : markForTranslation("%(_player_)s has captured all relics and will win in %(time)s."), "players": others, "parameters": { "_player_": cmpPlayer.GetPlayerID() }, "translateMessage": true, "translateParameters": [] }, captureTheRelicDuration); this.othersRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({ "message": isTeam ? markForTranslation("You and your allies have captured all relics and will win in %(time)s.") : markForTranslation("You have captured all relics and will win in %(time)s."), "players": winningPlayers, "translateMessage": true }, captureTheRelicDuration); this.relicsVictoryTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "CaptureTheRelicVictorySetWinner", captureTheRelicDuration, winningPlayers); }; Trigger.prototype.CaptureTheRelicVictorySetWinner = function(winningPlayers) { let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); cmpEndGameManager.MarkPlayersAsWon( winningPlayers, n => markForPluralTranslation( "%(lastPlayer)s has won (Capture the Relic).", "%(players)s and %(lastPlayer)s have won (Capture the Relic).", n), n => markForPluralTranslation( "%(lastPlayer)s has been defeated (Capture the Relic).", "%(players)s and %(lastPlayer)s have been defeated (Capture the Relic).", n)); }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.relics = []; cmpTrigger.playerRelicsCount = []; cmpTrigger.relicsVictoryTimer = undefined; cmpTrigger.ownRelicsVictoryMessage = undefined; cmpTrigger.othersRelicsVictoryMessage = undefined; cmpTrigger.relicsVictoryCountdownPlayers = []; cmpTrigger.DoAfterDelay(0, "InitCaptureTheRelic", {}); cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "CheckCaptureTheRelicCountdown", { "enabled": true }); cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckCaptureTheRelicVictory", { "enabled": true }); cmpTrigger.RegisterTrigger("OnPlayerWon", "DeleteCaptureTheRelicVictoryMessages", { "enabled": true }); cmpTrigger.RegisterTrigger("OnPlayerDefeated", "CheckCaptureTheRelicCountdown", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/maps/scripts/WonderVictory.js (revision 27722) @@ -1,156 +1,156 @@ Trigger.prototype.WonderVictoryEntityRenamed = function(data) { if (this.wonderVictoryMessages[data.entity] && Engine.QueryInterface(data.newentity, IID_Wonder)) { // When an entity is renamed, we first create a new entity, // which in case it is a wonder will receive a timer. // However on a rename we want to use the timer from the old entity, // so we need to remove the timer of the new entity. this.WonderVictoryDeleteTimer(data.newentity); this.wonderVictoryMessages[data.newentity] = this.wonderVictoryMessages[data.entity]; delete this.wonderVictoryMessages[data.entity]; } }; Trigger.prototype.WonderVictoryOwnershipChanged = function(data) { if (!Engine.QueryInterface(data.entity, IID_Wonder)) return; this.WonderVictoryDeleteTimer(data.entity); if (data.to > 0) this.WonderVictoryStartTimer(data.entity, data.to); }; Trigger.prototype.WonderVictoryDiplomacyChanged = function(data) { if (!Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).GetAlliedVictory()) return; for (let ent in this.wonderVictoryMessages) { if (this.wonderVictoryMessages[ent].playerID != data.player && this.wonderVictoryMessages[ent].playerID != data.otherPlayer) continue; let owner = this.wonderVictoryMessages[ent].playerID; let otherPlayer = owner == data.player ? data.otherPlayer : data.player; - let newAllies = new Set(QueryPlayerIDInterface(owner).GetPlayersByDiplomacy("IsExclusiveMutualAlly")); + let newAllies = new Set(QueryPlayerIDInterface(owner, IID_Diplomacy).GetPlayersByDiplomacy("IsExclusiveMutualAlly")); if (newAllies.has(otherPlayer) && !this.wonderVictoryMessages[ent].allies.has(otherPlayer) || !newAllies.has(otherPlayer) && this.wonderVictoryMessages[ent].allies.has(otherPlayer)) { this.WonderVictoryDeleteTimer(ent); this.WonderVictoryStartTimer(ent, owner); } } }; /** * Create new messages, and start timer to register defeat. */ Trigger.prototype.WonderVictoryStartTimer = function(ent, player) { let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); let allies = cmpEndGameManager.GetAlliedVictory() ? - QueryPlayerIDInterface(player).GetPlayersByDiplomacy("IsExclusiveMutualAlly") : []; + QueryPlayerIDInterface(player, IID_Diplomacy).GetPlayersByDiplomacy("IsExclusiveMutualAlly") : []; let others = [-1]; for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer.GetState() == "won") return; if (allies.indexOf(playerID) == -1 && playerID != player) others.push(playerID); } let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let wonderDuration = cmpEndGameManager.GetGameSettings().wonderDuration; this.wonderVictoryMessages[ent] = { "playerID": player, "allies": new Set(allies), "timer": cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "WonderVictorySetWinner", wonderDuration, player), "messages": [ cmpGuiInterface.AddTimeNotification( { "message": allies.length ? markForTranslation("%(_player_)s owns a Wonder and %(_player_)s and their allies will win in %(time)s.") : markForTranslation("%(_player_)s owns a Wonder and will win in %(time)s."), "players": others, "parameters": { "_player_": player }, "translateMessage": true, "translateParameters": [] }, wonderDuration), cmpGuiInterface.AddTimeNotification( { "message": markForTranslation("%(_player_)s owns a Wonder and you will win in %(time)s."), "players": allies, "parameters": { "_player_": player }, "translateMessage": true, "translateParameters": [] }, wonderDuration), cmpGuiInterface.AddTimeNotification( { "message": allies.length ? markForTranslation("You own a Wonder and you and your allies will win in %(time)s.") : markForTranslation("You own a Wonder and will win in %(time)s."), "players": [player], "translateMessage": true }, wonderDuration) ] }; }; Trigger.prototype.WonderVictoryDeleteTimer = function(ent) { if (!this.wonderVictoryMessages[ent]) return; let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); for (let message of this.wonderVictoryMessages[ent].messages) cmpGuiInterface.DeleteTimeNotification(message); cmpTimer.CancelTimer(this.wonderVictoryMessages[ent].timer); delete this.wonderVictoryMessages[ent]; }; Trigger.prototype.WonderVictoryPlayerWon = function(data) { for (let ent in this.wonderVictoryMessages) this.WonderVictoryDeleteTimer(ent); }; Trigger.prototype.WonderVictorySetWinner = function(playerID) { let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); cmpEndGameManager.MarkPlayerAndAlliesAsWon( playerID, n => markForPluralTranslation( "%(lastPlayer)s has won (wonder victory).", "%(players)s and %(lastPlayer)s have won (wonder victory).", n), n => markForPluralTranslation( "%(lastPlayer)s has been defeated (wonder victory).", "%(players)s and %(lastPlayer)s have been defeated (wonder victory).", n)); }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.RegisterTrigger("OnEntityRenamed", "WonderVictoryEntityRenamed", { "enabled": true }); cmpTrigger.RegisterTrigger("OnOwnershipChanged", "WonderVictoryOwnershipChanged", { "enabled": true }); cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "WonderVictoryDiplomacyChanged", { "enabled": true }); cmpTrigger.RegisterTrigger("OnPlayerWon", "WonderVictoryPlayerWon", { "enabled": true }); cmpTrigger.wonderVictoryMessages = {}; } Index: ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js (revision 27722) @@ -1,146 +1,146 @@ function AlertRaiser() {} AlertRaiser.prototype.Schema = "" + "" + "tokens" + "" + "" + "" + "" + "" + ""; AlertRaiser.prototype.Init = function() { // Store the last time the alert was used so players can't lag the game by raising alerts repeatedly. this.lastTime = 0; }; AlertRaiser.prototype.UnitFilter = function(unit) { let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), this.template.List._string); }; AlertRaiser.prototype.RaiseAlert = function() { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (cmpTimer.GetTime() == this.lastTime) return; this.lastTime = cmpTimer.GetTime(); PlaySound("alert_raise", this.entity); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return; let owner = cmpOwnership.GetOwner(); - let cmpPlayer = QueryOwnerInterface(this.entity); - let mutualAllies = cmpPlayer ? cmpPlayer.GetMutualAllies() : [owner]; + const cmpDiplomacy = QueryPlayerIDInterface(owner, IID_Diplomacy); + const mutualAllies = cmpDiplomacy ? cmpDiplomacy.GetMutualAllies() : [owner]; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); // Store the number of available garrison spots so that units don't try to garrison in buildings that will be full let reserved = new Map(); let units = cmpRangeManager.ExecuteQuery(this.entity, 0, +this.template.RaiseAlertRange, [owner], IID_UnitAI, true).filter(ent => this.UnitFilter(ent)); for (let unit of units) { let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable); if (!cmpGarrisonable) continue; let size = cmpGarrisonable.TotalSize(); let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); let holder = cmpRangeManager.ExecuteQuery(unit, 0, +this.template.SearchRange, mutualAllies, IID_GarrisonHolder, true).find(ent => { // Ignore moving garrison holders if (Engine.QueryInterface(ent, IID_UnitAI)) return false; // Ensure that the garrison holder is within range of the alert raiser if (+this.template.EndOfAlertRange > 0 && PositionHelper.DistanceBetweenEntities(this.entity, ent) > +this.template.EndOfAlertRange) return false; if (!cmpUnitAI.CheckTargetVisible(ent)) return false; let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (!reserved.has(ent)) reserved.set(ent, cmpGarrisonHolder.GetCapacity() - cmpGarrisonHolder.OccupiedSlots()); return cmpGarrisonHolder.IsAllowedToGarrison(unit) && reserved.get(ent) >= size; }); if (holder) { reserved.set(holder, reserved.get(holder) - size); cmpUnitAI.Garrison(holder, false, false); } else // If no available spots, stop moving cmpUnitAI.ReplaceOrder("Stop", { "force": true }); } }; AlertRaiser.prototype.EndOfAlert = function() { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (cmpTimer.GetTime() == this.lastTime) return; this.lastTime = cmpTimer.GetTime(); PlaySound("alert_end", this.entity); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return; let owner = cmpOwnership.GetOwner(); - let cmpPlayer = QueryOwnerInterface(this.entity); - let mutualAllies = cmpPlayer ? cmpPlayer.GetMutualAllies() : [owner]; + const cmpDiplomacy = QueryPlayerIDInterface(owner, IID_Diplomacy); + const mutualAllies = cmpDiplomacy ? cmpDiplomacy.GetMutualAllies() : [owner]; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); // Units that are not garrisoned should go back to work let units = cmpRangeManager.ExecuteQuery(this.entity, 0, +this.template.EndOfAlertRange, [owner], IID_UnitAI, true).filter(ent => this.UnitFilter(ent)); for (let unit of units) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (cmpUnitAI.HasWorkOrders() && cmpUnitAI.ShouldRespondToEndOfAlert()) cmpUnitAI.BackToWork(); else if (cmpUnitAI.ShouldRespondToEndOfAlert()) // Stop rather than continue to try to garrison cmpUnitAI.ReplaceOrder("Stop", { "force": true }); } // Units that are garrisoned should ungarrison and go back to work let holders = cmpRangeManager.ExecuteQuery(this.entity, 0, +this.template.EndOfAlertRange, mutualAllies, IID_GarrisonHolder, true); if (Engine.QueryInterface(this.entity, IID_GarrisonHolder)) holders.push(this.entity); for (let holder of holders) { if (Engine.QueryInterface(holder, IID_UnitAI)) continue; let cmpGarrisonHolder = Engine.QueryInterface(holder, IID_GarrisonHolder); let units = cmpGarrisonHolder.GetEntities().filter(ent => { let cmpOwner = Engine.QueryInterface(ent, IID_Ownership); return cmpOwner && cmpOwner.GetOwner() == owner && this.UnitFilter(ent); }); for (let unit of units) if (cmpGarrisonHolder.Unload(unit)) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (cmpUnitAI.HasWorkOrders()) cmpUnitAI.BackToWork(); else // Stop rather than walk to the rally point cmpUnitAI.ReplaceOrder("Stop", { "force": true }); } } }; Engine.RegisterComponentType(IID_AlertRaiser, "AlertRaiser", AlertRaiser); Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 27722) @@ -1,782 +1,783 @@ function Attack() {} var g_AttackTypes = ["Melee", "Ranged", "Capture"]; Attack.prototype.preferredClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.restrictedClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + "" + "" + "Spear" + "" + "10.0" + "0.0" + "5.0" + "" + "4.0" + "1000" + "" + "" + "pers" + "Infantry" + "1.5" + "" + "" + "Cavalry Melee" + "1.5" + "" + "" + "Champion" + "Cavalry Infantry" + "" + "" + "Bow" + "" + "0.0" + "10.0" + "0.0" + "" + "44.0" + "20.0" + "" + "0" + "10.0" + "0" + "" + "800" + "1600" + "1000" + "" + "" + "Cavalry" + "2" + "" + "" + "" + "50.0" + "2.5" + "props/units/weapons/rock_flaming.xml" + "props/units/weapons/rock_explosion.xml" + "0.1" + "false" + "" + "Champion" + "" + "Circular" + "20" + "false" + "" + "0.0" + "10.0" + "0.0" + "" + "" + "" + "" + "" + "1000.0" + "0.0" + "0.0" + "" + "1000" + "4.0" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + AttackHelper.BuildAttackEffectsSchema() + "" + "" + "" + "" + ""+ "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + AttackHelper.BuildAttackEffectsSchema() + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + ""; Attack.prototype.Init = function() { }; Attack.prototype.GetAttackTypes = function(wantedTypes) { let types = g_AttackTypes.filter(type => !!this.template[type]); if (!wantedTypes) return types; let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0); return types.filter(type => wantedTypes.indexOf("!" + type) == -1 && (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1)); }; Attack.prototype.GetPreferredClasses = function(type) { if (this.template[type] && this.template[type].PreferredClasses && this.template[type].PreferredClasses._string) return this.template[type].PreferredClasses._string.split(/\s+/); return []; }; Attack.prototype.GetRestrictedClasses = function(type) { if (this.template[type] && this.template[type].RestrictedClasses && this.template[type].RestrictedClasses._string) return this.template[type].RestrictedClasses._string.split(/\s+/); return []; }; Attack.prototype.CanAttack = function(target, wantedTypes) { const cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) return true; const cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); const cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) return false; const cmpResistance = QueryMiragedInterface(target, IID_Resistance); if (!cmpResistance) return false; const cmpIdentity = QueryMiragedInterface(target, IID_Identity); if (!cmpIdentity) return false; const cmpHealth = QueryMiragedInterface(target, IID_Health); const targetClasses = cmpIdentity.GetClassesList(); if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() && (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length || wantedTypes.indexOf("Slaughter") != -1)) return true; const cmpEntityPlayer = QueryOwnerInterface(this.entity); const cmpTargetPlayer = QueryOwnerInterface(target); if (!cmpTargetPlayer || !cmpEntityPlayer) return false; const types = this.GetAttackTypes(wantedTypes); const entityOwner = cmpEntityPlayer.GetPlayerID(); const targetOwner = cmpTargetPlayer.GetPlayerID(); const cmpCapturable = QueryMiragedInterface(target, IID_Capturable); + const cmpDiplomacy = QueryPlayerIDInterface(entityOwner, IID_Diplomacy); // Check if the relative height difference is larger than the attack range // If the relative height is bigger, it means they will never be able to // reach each other, no matter how close they come. const heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); for (const type of types) { - if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) + if (type != "Capture" && (!cmpDiplomacy?.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) continue; if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) continue; if (heightDiff > this.GetRange(type).max) continue; const restrictedClasses = this.GetRestrictedClasses(type); if (!restrictedClasses.length) return true; if (!MatchesClassList(targetClasses, restrictedClasses)) return true; } return false; }; /** * Returns undefined if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; let targetClasses = cmpIdentity.GetClassesList(); let minPref; for (let type of this.GetAttackTypes()) { let preferredClasses = this.GetPreferredClasses(type); for (let pref = 0; pref < preferredClasses.length; ++pref) { if (MatchesClassList(targetClasses, preferredClasses[pref])) { if (pref === 0) return pref; if ((minPref === undefined || minPref > pref)) minPref = pref; } } } return minPref; }; /** * Get the full range of attack using all available attack types. */ Attack.prototype.GetFullAttackRange = function() { let ret = { "min": Infinity, "max": 0 }; for (let type of this.GetAttackTypes()) { let range = this.GetRange(type); ret.min = Math.min(ret.min, range.min); ret.max = Math.max(ret.max, range.max); } return ret; }; Attack.prototype.GetAttackEffectsData = function(type, splash) { let template = this.template[type]; if (!template) return undefined; if (splash) template = template.Splash; return AttackHelper.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity); }; /** * Find the best attack against a target. * @param {number} target - The entity-ID of the target. * @param {boolean} allowCapture - Whether capturing is allowed. * @return {string} - The preferred attack type. */ Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) { let types = this.GetAttackTypes(); if (Engine.QueryInterface(target, IID_Formation)) // TODO: Formation against formation needs review return g_AttackTypes.find(attack => types.indexOf(attack) != -1); const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; // Always slaughter domestic animals instead of using a normal attack if (this.template.Slaughter && cmpIdentity.HasClass("Domestic")) return "Slaughter"; const targetClasses = cmpIdentity.GetClassesList(); const getPreferrence = attackType => { let pref = 0; if (MatchesClassList(targetClasses, this.GetPreferredClasses(attackType))) pref += 2; if (allowCapture ? attackType === "Capture" : attackType !== "Capture") pref++; return pref; }; return types.filter(type => this.CanAttack(target, [type])).sort((a, b) => { const prefA = getPreferrence(a); const prefB = getPreferrence(b); return (types.indexOf(a) + (prefA > 0 ? prefA + types.length : 0)) - (types.indexOf(b) + (prefB > 0 ? prefB + types.length : 0)) }).pop(); }; Attack.prototype.CompareEntitiesByPreference = function(a, b) { let aPreference = this.GetPreference(a); let bPreference = this.GetPreference(b); if (aPreference === null && bPreference === null) return 0; if (aPreference === null) return 1; if (bPreference === null) return -1; return aPreference - bPreference; }; Attack.prototype.GetAttackName = function(type) { return { "name": this.template[type].AttackName._string || this.template[type].AttackName, "context": this.template[type].AttackName["@context"] }; }; Attack.prototype.GetRepeatTime = function(type) { let repeatTime = 1000; if (this.template[type] && this.template[type].RepeatTime) repeatTime = +this.template[type].RepeatTime; return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeatTime, this.entity); }; Attack.prototype.GetTimers = function(type) { return { "prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity), "repeat": this.GetRepeatTime(type) }; }; Attack.prototype.GetSplashData = function(type) { if (!this.template[type].Splash) return undefined; return { "attackData": this.GetAttackEffectsData(type, true), "friendlyFire": this.template[type].Splash.FriendlyFire == "true", "radius": ApplyValueModificationsToEntity("Attack/" + type + "/Splash/Range", +this.template[type].Splash.Range, this.entity), "shape": this.template[type].Splash.Shape, }; }; Attack.prototype.GetRange = function(type) { if (!type) return this.GetFullAttackRange(); let max = +this.template[type].MaxRange; max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity); let min = +(this.template[type].MinRange || 0); min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); return { "max": max, "min": min }; }; Attack.prototype.GetAttackYOrigin = function(type) { if (!this.template[type].Origin) return 0; return ApplyValueModificationsToEntity("Attack/" + type + "/Origin/Y", +this.template[type].Origin.Y, this.entity); }; /** * @param {number} target - The target to attack. * @param {string} type - The type of attack to use. * @param {number} callerIID - The IID to notify on specific events. * * @return {boolean} - Whether we started attacking. */ Attack.prototype.StartAttacking = function(target, type, callerIID) { if (this.target) this.StopAttacking(); if (!this.CanAttack(target, [type])) return false; const cmpResistance = QueryMiragedInterface(target, IID_Resistance); if (!cmpResistance || !cmpResistance.AddAttacker(this.entity)) return false; let timings = this.GetTimers(type); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); // If the repeat time since the last attack hasn't elapsed, // delay the action to avoid attacking too fast. let prepare = timings.prepare; if (this.lastAttacked) { let repeatLeft = this.lastAttacked + timings.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { cmpVisual.SelectAnimation("attack_" + type.toLowerCase(), false, 1.0); cmpVisual.SetAnimationSyncRepeat(timings.repeat); cmpVisual.SetAnimationSyncOffset(prepare); } // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = prepare != timings.prepare; this.target = target; this.callerIID = callerIID; this.timer = cmpTimer.SetInterval(this.entity, IID_Attack, "Attack", prepare, timings.repeat, type); return true; }; /** * @param {string} reason - The reason why we stopped attacking. */ Attack.prototype.StopAttacking = function(reason) { if (!this.target) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); delete this.timer; const cmpResistance = QueryMiragedInterface(this.target, IID_Resistance); if (cmpResistance) cmpResistance.RemoveAttacker(this.entity); delete this.target; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; if (reason && callerIID) { let component = Engine.QueryInterface(this.entity, callerIID); if (component) component.ProcessMessage(reason, null); } }; /** * Attack our target entity. * @param {string} data - The attack type to use. * @param {number} lateness - The offset of the actual call and when it was expected. */ Attack.prototype.Attack = function(type, lateness) { if (!this.CanAttack(this.target, [type])) { this.StopAttacking("TargetInvalidated"); return; } // ToDo: Enable entities to keep facing a target. Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastAttacked = cmpTimer.GetTime() - lateness; // BuildingAI has its own attack routine. if (!Engine.QueryInterface(this.entity, IID_BuildingAI)) this.PerformAttack(type, this.target); if (!this.target) return; // We check the range after the attack to facilitate chasing. if (!this.IsTargetInRange(this.target, type)) { this.StopAttacking("OutOfRange"); return; } if (this.resyncAnimation) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { let repeat = this.GetTimers(type).repeat; cmpVisual.SetAnimationSyncRepeat(repeat); cmpVisual.SetAnimationSyncOffset(repeat); } delete this.resyncAnimation; } }; /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ Attack.prototype.PerformAttack = function(type, target) { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; let selfPosition = cmpPosition.GetPosition(); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; let targetPosition = cmpTargetPosition.GetPosition(); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return; let attackerOwner = cmpOwnership.GetOwner(); let data = { "type": type, "attackData": this.GetAttackEffectsData(type), "splash": this.GetSplashData(type), "attacker": this.entity, "attackerOwner": attackerOwner, "target": target, }; let delay = +(this.template[type].EffectDelay || 0); if (this.template[type].Projectile) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let turnLength = cmpTimer.GetLatestTurnLength()/1000; // In the future this could be extended: // * Obstacles like trees could reduce the probability of the target being hit // * Obstacles like walls should block projectiles entirely let horizSpeed = +this.template[type].Projectile.Speed; let gravity = +this.template[type].Projectile.Gravity; // horizSpeed /= 2; gravity /= 2; // slow it down for testing // We will try to estimate the position of the target, where we can hit it. // We first estimate the time-till-hit by extrapolating linearly the movement // of the last turn. We compute the time till an arrow will intersect the target. let targetVelocity = Vector3D.sub(targetPosition, cmpTargetPosition.GetPreviousPosition()).div(turnLength); let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); // 'Cheat' and use UnitMotion to predict the position in the near-future. // This avoids 'dancing' issues with units zigzagging over very short distances. // However, this could fail if the player gives several short move orders, so // occasionally fall back to basic interpolation. let predictedPosition = targetPosition; if (timeToTarget !== false) { // Don't predict too far in the future, but avoid threshold effects. // After 1 second, always use the 'dumb' interpolated past-motion prediction. let useUnitMotion = randBool(Math.max(0, 0.75 - timeToTarget / 1.333)); if (useUnitMotion) { let cmpTargetUnitMotion = Engine.QueryInterface(target, IID_UnitMotion); let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpTargetUnitMotion && (!cmpTargetUnitAI || !cmpTargetUnitAI.IsFormationMember())) { let pos2D = cmpTargetUnitMotion.EstimateFuturePosition(timeToTarget); predictedPosition.x = pos2D.x; predictedPosition.z = pos2D.y; } else predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); } else predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); } let predictedHeight = cmpTargetPosition.GetHeightAt(predictedPosition.x, predictedPosition.z); // Add inaccuracy based on spread. let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/" + type + "/Spread", +this.template[type].Projectile.Spread, this.entity) * predictedPosition.horizDistanceTo(selfPosition) / 100; let randNorm = randomNormal2D(); let offsetX = randNorm[0] * distanceModifiedSpread; let offsetZ = randNorm[1] * distanceModifiedSpread; data.position = new Vector3D(predictedPosition.x + offsetX, predictedHeight, predictedPosition.z + offsetZ); let realHorizDistance = data.position.horizDistanceTo(selfPosition); timeToTarget = realHorizDistance / horizSpeed; delay += timeToTarget * 1000; data.direction = Vector3D.sub(data.position, selfPosition).div(realHorizDistance); let actorName = this.template[type].Projectile.ActorName || ""; let impactActorName = this.template[type].Projectile.ImpactActorName || ""; let impactAnimationLifetime = this.template[type].Projectile.ImpactAnimationLifetime || 0; // TODO: Use unit rotation to implement x/z offsets. let deltaLaunchPoint = new Vector3D(0, +this.template[type].Projectile.LaunchPoint["@y"], 0); let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { // if the projectile definition is missing from the template // then fallback to the projectile name and launchpoint in the visual actor if (!actorName) actorName = cmpVisual.GetProjectileActor(); let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint(); if (visualActorLaunchPoint.length() > 0) launchPoint = visualActorLaunchPoint; } let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); data.projectileId = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, data.position, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime); let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); data.attackImpactSound = cmpSound ? cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()) : ""; data.friendlyFire = this.template[type].Projectile.FriendlyFire == "true"; } else { data.position = targetPosition; data.direction = Vector3D.sub(targetPosition, selfPosition); } if (delay) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "Hit", delay, data); } else Engine.QueryInterface(SYSTEM_ENTITY, IID_DelayedDamage).Hit(data, 0); }; /** * @param {number} - The entity ID of the target to check. * @return {boolean} - Whether this entity is in range of its target. */ Attack.prototype.IsTargetInRange = function(target, type) { const range = this.GetRange(type); return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetParabolicRange( this.entity, target, range.min, range.max, this.GetAttackYOrigin(type), false); }; Attack.prototype.OnValueModification = function(msg) { if (msg.component != "Attack") return; let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (!cmpUnitAI) return; if (this.GetAttackTypes().some(type => msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1)) cmpUnitAI.UpdateRangeQueries(); }; Attack.prototype.GetRangeOverlays = function(type = "Ranged") { if (!this.template[type] || !this.template[type].RangeOverlay) return []; let range = this.GetRange(type); let rangeOverlays = []; for (let i in range) if ((i == "min" || i == "max") && range[i]) rangeOverlays.push({ "radius": range[i], "texture": this.template[type].RangeOverlay.LineTexture, "textureMask": this.template[type].RangeOverlay.LineTextureMask, "thickness": +this.template[type].RangeOverlay.LineThickness, }); return rangeOverlays; }; Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: ps/trunk/binaries/data/mods/public/simulation/components/Auras.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 27722) @@ -1,535 +1,539 @@ function Auras() {} Auras.prototype.Schema = "" + "tokens" + "" + ""; Auras.prototype.Init = function() { this.affectedPlayers = {}; for (let name of this.GetAuraNames()) this.affectedPlayers[name] = []; // In case of autogarrisoning, this component can be called before ownership is set. // So it needs to be completely initialised from the start. this.Clean(); }; // We can modify identifier if we want stackable auras in some case. Auras.prototype.GetModifierIdentifier = function(name) { if (AuraTemplates.Get(name).stackable) return "aura/" + name + this.entity; return "aura/" + name; }; Auras.prototype.GetDescriptions = function() { var ret = {}; for (let auraID of this.GetAuraNames()) { let aura = AuraTemplates.Get(auraID); ret[auraID] = { "name": { "generic": aura.auraName }, "description": aura.auraDescription || null, "radius": this.GetRange(auraID) || null }; } return ret; }; Auras.prototype.GetAuraNames = function() { return this.template._string.split(/\s+/); }; Auras.prototype.GetOverlayIcon = function(name) { return AuraTemplates.Get(name).overlayIcon || ""; }; Auras.prototype.GetAffectedEntities = function(name) { return this[name].targetUnits; }; Auras.prototype.GetRange = function(name) { if (this.IsRangeAura(name)) return +AuraTemplates.Get(name).radius; return undefined; }; Auras.prototype.GetClasses = function(name) { return AuraTemplates.Get(name).affects; }; Auras.prototype.GetModifications = function(name) { return AuraTemplates.Get(name).modifications; }; Auras.prototype.GetAffectedPlayers = function(name) { return this.affectedPlayers[name]; }; Auras.prototype.GetRangeOverlays = function() { let rangeOverlays = []; for (let name of this.GetAuraNames()) { if (!this.IsRangeAura(name) || !this[name].isApplied) continue; let rangeOverlay = AuraTemplates.Get(name).rangeOverlay; rangeOverlays.push( rangeOverlay ? { "radius": this.GetRange(name), "texture": rangeOverlay.lineTexture, "textureMask": rangeOverlay.lineTextureMask, "thickness": rangeOverlay.lineThickness } : // Specify default in order not to specify it in about 40 auras { "radius": this.GetRange(name), "texture": "outline_border.png", "textureMask": "outline_border_mask.png", "thickness": 0.2 }); } return rangeOverlays; }; Auras.prototype.CalculateAffectedPlayers = function(name) { var affectedPlayers = AuraTemplates.Get(name).affectedPlayers || ["Player"]; this.affectedPlayers[name] = []; var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer || cmpPlayer.IsDefeated()) return; + const playerID = cmpPlayer.GetPlayerID(); + const cmpDiplomacy = Engine.QueryInterface(this.entity, IID_Diplomacy) ?? + QueryPlayerIDInterface(playerID, IID_Diplomacy); + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); for (let i of cmpPlayerManager.GetAllPlayers()) { let cmpAffectedPlayer = QueryPlayerIDInterface(i); if (!cmpAffectedPlayer || cmpAffectedPlayer.IsDefeated()) continue; - if (affectedPlayers.some(p => p == "Player" ? cmpPlayer.GetPlayerID() == i : cmpPlayer["Is" + p](i))) + if (affectedPlayers.some(p => p == "Player" ? playerID == i : cmpDiplomacy["Is" + p](i))) this.affectedPlayers[name].push(i); } }; Auras.prototype.CanApply = function(name) { if (!AuraTemplates.Get(name).requiredTechnology) return true; let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(AuraTemplates.Get(name).requiredTechnology); }; Auras.prototype.HasFormationAura = function() { return this.GetAuraNames().some(n => this.IsFormationAura(n)); }; Auras.prototype.HasGarrisonAura = function() { return this.GetAuraNames().some(n => this.IsGarrisonAura(n)); }; Auras.prototype.HasGarrisonedUnitsAura = function() { return this.GetAuraNames().some(n => this.IsGarrisonedUnitsAura(n)); }; Auras.prototype.GetType = function(name) { return AuraTemplates.Get(name).type; }; Auras.prototype.IsFormationAura = function(name) { return this.GetType(name) == "formation"; }; Auras.prototype.IsGarrisonAura = function(name) { return this.GetType(name) == "garrison"; }; Auras.prototype.IsGarrisonedUnitsAura = function(name) { return this.GetType(name) == "garrisonedUnits"; }; Auras.prototype.IsTurretedUnitsAura = function(name) { return this.GetType(name) == "turretedUnits"; }; Auras.prototype.IsRangeAura = function(name) { return this.GetType(name) == "range"; }; Auras.prototype.IsGlobalAura = function(name) { return this.GetType(name) == "global"; }; Auras.prototype.IsPlayerAura = function(name) { return this.GetType(name) == "player"; }; /** * clean all bonuses. Remove the old ones and re-apply the new ones */ Auras.prototype.Clean = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var auraNames = this.GetAuraNames(); let targetUnitsClone = {}; let needVisualizationUpdate = false; // remove all bonuses for (let name of auraNames) { targetUnitsClone[name] = []; if (!this[name]) continue; if (this.IsRangeAura(name)) needVisualizationUpdate = true; if (this[name].targetUnits) targetUnitsClone[name] = this[name].targetUnits.slice(); if (this.IsGlobalAura(name)) this.RemoveTemplateAura(name); this.RemoveAura(name, this[name].targetUnits); if (this[name].rangeQuery) cmpRangeManager.DestroyActiveQuery(this[name].rangeQuery); } for (let name of auraNames) { // only calculate the affected players on re-applying the bonuses // this makes sure the template bonuses are removed from the correct players this.CalculateAffectedPlayers(name); // initialise range query this[name] = {}; this[name].targetUnits = []; this[name].isApplied = this.CanApply(name); var affectedPlayers = this.GetAffectedPlayers(name); if (!affectedPlayers.length) continue; if (this.IsGlobalAura(name)) { this.ApplyTemplateAura(name, affectedPlayers); // Only need to call ApplyAura for the aura icons, so skip it if there are none. if (this.GetOverlayIcon(name)) for (let player of affectedPlayers) this.ApplyAura(name, cmpRangeManager.GetEntitiesByPlayer(player)); continue; } if (this.IsPlayerAura(name)) { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); this.ApplyAura(name, affectedPlayers.map(p => cmpPlayerManager.GetPlayerByID(p))); continue; } if (!this.IsRangeAura(name)) { this.ApplyAura(name, targetUnitsClone[name]); continue; } needVisualizationUpdate = true; if (this[name].isApplied && (this.IsRangeAura(name) || this.IsGlobalAura(name) && !!this.GetOverlayIcon(name))) { // Do not account for entity sizes: structures can have various sizes // and we currently prefer auras to not depend on the source size // (this is generally irrelevant for units). this[name].rangeQuery = cmpRangeManager.CreateActiveQuery( this.entity, 0, this.GetRange(name), affectedPlayers, IID_Identity, cmpRangeManager.GetEntityFlagMask("normal"), false ); cmpRangeManager.EnableActiveQuery(this[name].rangeQuery); } } if (needVisualizationUpdate) { let cmpRangeOverlayManager = Engine.QueryInterface(this.entity, IID_RangeOverlayManager); if (cmpRangeOverlayManager) { cmpRangeOverlayManager.UpdateRangeOverlays("Auras"); cmpRangeOverlayManager.RegenerateRangeOverlays(false); } } }; Auras.prototype.GiveMembersWithValidClass = function(auraName, entityList) { var match = this.GetClasses(auraName); return entityList.filter(ent => { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), match); }); }; Auras.prototype.OnRangeUpdate = function(msg) { for (let name of this.GetAuraNames().filter(n => this[n] && msg.tag == this[n].rangeQuery)) { this.ApplyAura(name, msg.added); this.RemoveAura(name, msg.removed); } }; Auras.prototype.OnGarrisonedUnitsChanged = function(msg) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonedUnitsAura(n))) { this.ApplyAura(name, msg.added); this.RemoveAura(name, msg.removed); } }; Auras.prototype.OnTurretsChanged = function(msg) { for (let name of this.GetAuraNames().filter(n => this.IsTurretedUnitsAura(n))) { this.ApplyAura(name, msg.added); this.RemoveAura(name, msg.removed); } }; Auras.prototype.ApplyFormationAura = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) this.ApplyAura(name, memberList); }; Auras.prototype.ApplyGarrisonAura = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) this.ApplyAura(name, [structure]); }; Auras.prototype.ApplyTemplateAura = function(name, players) { if (!this[name].isApplied) return; if (!this.IsGlobalAura(name)) return; let derivedModifiers = DeriveModificationsFromTech({ "modifications": this.GetModifications(name), "affects": this.GetClasses(name) }); let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let modifName = this.GetModifierIdentifier(name); for (let player of players) cmpModifiersManager.AddModifiers(modifName, derivedModifiers, cmpPlayerManager.GetPlayerByID(player)); }; Auras.prototype.RemoveFormationAura = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) this.RemoveAura(name, memberList); }; Auras.prototype.RemoveGarrisonAura = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) this.RemoveAura(name, [structure]); }; Auras.prototype.RemoveTemplateAura = function(name) { if (!this[name].isApplied) return; if (!this.IsGlobalAura(name)) return; let derivedModifiers = DeriveModificationsFromTech({ "modifications": this.GetModifications(name), "affects": this.GetClasses(name) }); let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let modifName = this.GetModifierIdentifier(name); for (let player of this.GetAffectedPlayers(name)) { let playerId = cmpPlayerManager.GetPlayerByID(player); for (let modifierPath in derivedModifiers) cmpModifiersManager.RemoveModifier(modifierPath, modifName, playerId); } }; Auras.prototype.ApplyAura = function(name, ents) { var validEnts = this.GiveMembersWithValidClass(name, ents); if (!validEnts.length) return; this[name].targetUnits = this[name].targetUnits.concat(validEnts); if (!this[name].isApplied) return; // update status bars if this has an icon if (this.GetOverlayIcon(name)) for (let ent of validEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.AddAuraSource(this.entity, name); } // Global aura modifications are handled at the player level by the modification manager, // so stop after icons have been applied. if (this.IsGlobalAura(name)) return; let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let derivedModifiers = DeriveModificationsFromTech({ "modifications": this.GetModifications(name), "affects": this.GetClasses(name) }); let modifName = this.GetModifierIdentifier(name); for (let ent of validEnts) cmpModifiersManager.AddModifiers(modifName, derivedModifiers, ent); }; Auras.prototype.RemoveAura = function(name, ents, skipModifications = false) { var validEnts = this.GiveMembersWithValidClass(name, ents); if (!validEnts.length) return; this[name].targetUnits = this[name].targetUnits.filter(v => validEnts.indexOf(v) == -1); if (!this[name].isApplied) return; // update status bars if this has an icon if (this.GetOverlayIcon(name)) for (let ent of validEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RemoveAuraSource(this.entity, name); } // Global aura modifications are handled at the player level by the modification manager, // so stop after icons have been removed. if (this.IsGlobalAura(name)) return; let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); let derivedModifiers = DeriveModificationsFromTech({ "modifications": this.GetModifications(name), "affects": this.GetClasses(name) }); let modifName = this.GetModifierIdentifier(name); for (let ent of ents) for (let modifierPath in derivedModifiers) cmpModifiersManager.RemoveModifier(modifierPath, modifName, ent); }; Auras.prototype.OnOwnershipChanged = function(msg) { this.Clean(); }; Auras.prototype.OnDiplomacyChanged = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && (cmpPlayer.GetPlayerID() == msg.player || cmpPlayer.GetPlayerID() == msg.otherPlayer) || IsOwnedByPlayer(msg.player, this.entity) || IsOwnedByPlayer(msg.otherPlayer, this.entity)) this.Clean(); }; Auras.prototype.OnGlobalResearchFinished = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if ((!cmpPlayer || cmpPlayer.GetPlayerID() != msg.player) && !IsOwnedByPlayer(msg.player, this.entity)) return; for (let name of this.GetAuraNames()) { let requiredTech = AuraTemplates.Get(name).requiredTechnology; if (requiredTech && requiredTech == msg.tech) { this.Clean(); return; } } }; /** * Update auras of the player entity and entities affecting player entities that didn't change ownership. */ Auras.prototype.OnGlobalPlayerDefeated = function(msg) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() == msg.playerId || this.GetAuraNames().some(name => this.GetAffectedPlayers(name).indexOf(msg.playerId) != -1)) this.Clean(); }; Auras.prototype.OnGarrisonedStateChanged = function(msg) { if (!this.HasGarrisonAura()) return; if (msg.holderID != INVALID_ENTITY) this.ApplyGarrisonAura(msg.holderID); if (msg.oldHolder != INVALID_ENTITY) this.RemoveGarrisonAura(msg.oldHolder); }; Engine.RegisterComponentType(IID_Auras, "Auras", Auras); Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js (revision 27722) @@ -1,330 +1,335 @@ function BuildRestrictions() {} BuildRestrictions.prototype.Schema = "Specifies building placement restrictions as they relate to terrain, territories, and distance." + "" + "" + "land" + "own" + "Structure" + "" + "CivilCentre" + "40" + "" + "" + "" + "" + "" + "land" + "shore" + "land-shore"+ "" + "" + "" + "" + "" + "" + "own" + "ally" + "neutral" + "enemy" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; BuildRestrictions.prototype.Init = function() { }; /** * Checks whether building placement is valid * 1. Visibility is not hidden (may be fogged or visible) * 2. Check foundation * a. Doesn't obstruct foundation-blocking entities * b. On valid terrain, based on passability class * 3. Territory type is allowed (see note below) * 4. Dock is on shoreline and facing into water * 5. Distance constraints satisfied * * Returns result object: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the GUI message * "translateMessage": always true * "translateParameters": list of parameters to translate * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": plural translation argument (optional) * } * * Note: The entity which is used to check this should be a preview entity * (template name should be "preview|"+templateName), as otherwise territory * checks for buildings with territory influence will not work as expected. */ BuildRestrictions.prototype.CheckPlacement = function() { var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building"; var result = { "success": false, "message": markForTranslation("%(name)s cannot be built due to unknown error"), "parameters": { "name": name, }, "translateMessage": true, "translateParameters": ["name"], }; var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (!cmpPlayer) return result; // Fail // TODO: AI has no visibility info if (!cmpPlayer.IsAI()) { // Check whether it's in a visible or fogged region var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpRangeManager || !cmpOwnership) return result; // Fail var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden"); if (!explored) { result.message = markForTranslation("%(name)s cannot be built in unexplored area"); return result; // Fail } } // Check obstructions and terrain passability var passClassName = ""; switch (this.template.PlacementType) { case "shore": passClassName = "building-shore"; break; case "land-shore": // 'default-terrain-only' is everywhere a normal unit can go, ignoring // obstructions (i.e. on passable land, and not too deep in the water) passClassName = "default-terrain-only"; break; case "land": default: passClassName = "building-land"; } var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return result; // Fail if (this.template.Category == "Wall") { // for walls, only test the center point var ret = cmpObstruction.CheckFoundation(passClassName, true); } else { var ret = cmpObstruction.CheckFoundation(passClassName, false); } if (ret != "success") { switch (ret) { case "fail_error": case "fail_no_obstruction": error("CheckPlacement: Error returned from CheckFoundation"); break; case "fail_obstructs_foundation": result.message = markForTranslation("%(name)s cannot be built on another building or resource"); break; case "fail_terrain_class": // TODO: be more specific and/or list valid terrain? result.message = markForTranslation("%(name)s cannot be built on invalid terrain"); } return result; // Fail } // Check territory restrictions var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpTerritoryManager || !cmpPosition || !cmpPosition.IsInWorld()) return result; // Fail + const playerID = cmpPlayer.GetPlayerID(); + const cmpDiplomacy = QueryPlayerIDInterface(playerID, IID_Diplomacy); + if (!cmpDiplomacy) + return result; + var pos = cmpPosition.GetPosition2D(); var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); var isConnected = !cmpTerritoryManager.IsTerritoryBlinking(pos.x, pos.y); - var isOwn = tileOwner == cmpPlayer.GetPlayerID(); - var isMutualAlly = cmpPlayer.IsExclusiveMutualAlly(tileOwner); + var isOwn = tileOwner == playerID; + var isMutualAlly = cmpDiplomacy.IsExclusiveMutualAlly(tileOwner); var isNeutral = tileOwner == 0; var invalidTerritory = ""; if (isOwn) { if (!this.HasTerritory("own")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "own"); else if (!isConnected && !this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own"); } else if (isMutualAlly) { if (!this.HasTerritory("ally")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "allied"); else if (!isConnected && !this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied"); } else if (isNeutral) { if (!this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "neutral"); } else { // consider everything else enemy territory if (!this.HasTerritory("enemy")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "enemy"); } if (invalidTerritory) { result.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s"); result.translateParameters.push("territoryType"); result.translateParameters.push("validTerritories"); result.parameters.territoryType = { "context": "Territory type", "_string": invalidTerritory }; // gui code will join this array to a string result.parameters.validTerritories = { "context": "Territory type list", "list": this.GetTerritories() }; return result; // Fail } // Check special requirements if (this.template.PlacementType == "shore") { if (!cmpObstruction.CheckShorePlacement()) { result.message = markForTranslation("%(name)s must be built on a valid shoreline"); return result; // Fail } } let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName)); // Check distance restriction if (this.template.Distance) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cat = this.template.Distance.FromClass; var filter = function(id) { var cmpIdentity = Engine.QueryInterface(id, IID_Identity); return cmpIdentity.GetClassesList().indexOf(cat) > -1; }; if (this.template.Distance.MinDistance !== undefined) { let minDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MinDistance", +this.template.Distance.MinDistance, cmpPlayer.GetPlayerID(), template); if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) { let result = markForPluralTranslation( "%(name)s too close to a %(category)s, must be at least %(distance)s meter away", "%(name)s too close to a %(category)s, must be at least %(distance)s meters away", minDistance); result.success = false; result.translateMessage = true; result.parameters = { "name": name, "category": cat, "distance": minDistance }; result.translateParameters = ["name", "category"]; return result; // Fail } } if (this.template.Distance.MaxDistance !== undefined) { let maxDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MaxDistance", +this.template.Distance.MaxDistance, cmpPlayer.GetPlayerID(), template); if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) { let result = markForPluralTranslation( "%(name)s too far from a %(category)s, must be within %(distance)s meter", "%(name)s too far from a %(category)s, must be within %(distance)s meters", maxDistance); result.success = false; result.translateMessage = true; result.parameters = { "name": name, "category": cat, "distance": maxDistance }; result.translateParameters = ["name", "category"]; return result; // Fail } } } // Success result.success = true; result.message = ""; return result; }; BuildRestrictions.prototype.GetCategory = function() { return this.template.Category; }; BuildRestrictions.prototype.GetTerritories = function() { return ApplyValueModificationsToEntity("BuildRestrictions/Territory", this.template.Territory, this.entity).split(/\s+/); }; BuildRestrictions.prototype.HasTerritory = function(territory) { return (this.GetTerritories().indexOf(territory) != -1); }; // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "own"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "ally"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "neutral"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "enemy"); Engine.RegisterComponentType(IID_BuildRestrictions, "BuildRestrictions", BuildRestrictions); Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 27722) @@ -1,384 +1,383 @@ // Number of rounds of firing per 2 seconds. const roundCount = 10; const attackType = "Ranged"; function BuildingAI() {} BuildingAI.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2; BuildingAI.prototype.Init = function() { this.currentRound = 0; this.archersGarrisoned = 0; this.arrowsLeft = 0; this.targetUnits = []; }; BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg) { let classes = this.template.GarrisonArrowClasses; for (let ent of msg.added) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes)) ++this.archersGarrisoned; } for (let ent of msg.removed) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes)) --this.archersGarrisoned; } }; BuildingAI.prototype.OnOwnershipChanged = function(msg) { this.targetUnits = []; this.SetupRangeQuery(); this.SetupGaiaRangeQuery(); }; BuildingAI.prototype.OnDiplomacyChanged = function(msg) { if (!IsOwnedByPlayer(msg.player, this.entity)) return; // Remove maybe now allied/neutral units. this.targetUnits = []; this.SetupRangeQuery(); this.SetupGaiaRangeQuery(); }; BuildingAI.prototype.OnDestroy = function() { if (this.timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } // Clean up range queries. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.enemyUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); if (this.gaiaUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery); }; /** * React on Attack value modifications, as it might influence the range. */ BuildingAI.prototype.OnValueModification = function(msg) { if (msg.component != "Attack") return; this.targetUnits = []; this.SetupRangeQuery(); this.SetupGaiaRangeQuery(); }; /** * Setup the Range Query to detect units coming in & out of range. */ BuildingAI.prototype.SetupRangeQuery = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.enemyUnitsQuery) { cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); this.enemyUnitsQuery = undefined; } - var cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) + const cmpDiplomacy = QueryOwnerInterface(this.entity, IID_Diplomacy); + if (!cmpDiplomacy) return; - var enemies = cmpPlayer.GetEnemies(); + const enemies = cmpDiplomacy.GetEnemies(); // Remove gaia. if (enemies.length && enemies[0] == 0) enemies.shift(); if (!enemies.length) return; const range = cmpAttack.GetRange(attackType); const yOrigin = cmpAttack.GetAttackYOrigin(attackType); // This takes entity sizes into accounts, so no need to compensate for structure size. this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery( this.entity, range.min, range.max, yOrigin, enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery); }; // Set up a range query for Gaia units within LOS range which can be attacked. // This should be called whenever our ownership changes. BuildingAI.prototype.SetupGaiaRangeQuery = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.gaiaUnitsQuery) { cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery); this.gaiaUnitsQuery = undefined; } - var cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer || !cmpPlayer.IsEnemy(0)) + if (!QueryOwnerInterface(this.entity, IID_Diplomacy)?.IsEnemy(0)) return; const range = cmpAttack.GetRange(attackType); const yOrigin = cmpAttack.GetAttackYOrigin(attackType); // This query is only interested in Gaia entities that can attack. // This takes entity sizes into accounts, so no need to compensate for structure size. this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery( this.entity, range.min, range.max, yOrigin, [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery); }; /** * Called when units enter or leave range. */ BuildingAI.prototype.OnRangeUpdate = function(msg) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; // Target enemy units except non-dangerous animals. if (msg.tag == this.gaiaUnitsQuery) { msg.added = msg.added.filter(e => { let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }); } else if (msg.tag != this.enemyUnitsQuery) return; // Add new targets. for (let entity of msg.added) if (cmpAttack.CanAttack(entity)) this.targetUnits.push(entity); // Remove targets outside of vision-range. for (let entity of msg.removed) { let index = this.targetUnits.indexOf(entity); if (index > -1) this.targetUnits.splice(index, 1); } if (this.targetUnits.length) this.StartTimer(); }; BuildingAI.prototype.StartTimer = function() { if (this.timer) return; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var attackTimers = cmpAttack.GetTimers(attackType); this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows", attackTimers.prepare, attackTimers.repeat / roundCount, null); }; BuildingAI.prototype.GetDefaultArrowCount = function() { var arrowCount = +this.template.DefaultArrowCount; return Math.round(ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity)); }; BuildingAI.prototype.GetMaxArrowCount = function() { if (!this.template.MaxArrowCount) return Infinity; let maxArrowCount = +this.template.MaxArrowCount; return Math.round(ApplyValueModificationsToEntity("BuildingAI/MaxArrowCount", maxArrowCount, this.entity)); }; BuildingAI.prototype.GetGarrisonArrowMultiplier = function() { var arrowMult = +this.template.GarrisonArrowMultiplier; return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity); }; BuildingAI.prototype.GetGarrisonArrowClasses = function() { var string = this.template.GarrisonArrowClasses; if (string) return string.split(/\s+/); return []; }; /** * Returns the number of arrows which needs to be fired. * DefaultArrowCount + Garrisoned Archers (i.e., any unit capable * of shooting arrows from inside buildings). */ BuildingAI.prototype.GetArrowCount = function() { let count = this.GetDefaultArrowCount() + Math.round(this.archersGarrisoned * this.GetGarrisonArrowMultiplier()); return Math.min(count, this.GetMaxArrowCount()); }; BuildingAI.prototype.SetUnitAITarget = function(ent) { this.unitAITarget = ent; if (ent) this.StartTimer(); }; /** * Fire arrows with random temporal distribution on prefered targets. * Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range. */ BuildingAI.prototype.FireArrows = function() { if (!this.targetUnits.length && !this.unitAITarget) { if (!this.timer) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; return; } let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; if (this.currentRound > roundCount - 1) this.currentRound = 0; if (this.currentRound == 0) this.arrowsLeft = this.GetArrowCount(); let arrowsToFire = 0; if (this.currentRound == roundCount - 1) arrowsToFire = this.arrowsLeft; else arrowsToFire = Math.min( randIntInclusive(0, 2 * this.GetArrowCount() / roundCount), this.arrowsLeft ); if (arrowsToFire <= 0) { ++this.currentRound; return; } // Add targets to a weighted list, to allow preferences. let targets = new WeightedList(); let maxPreference = this.MAX_PREFERENCE_BONUS; let addTarget = function(target) { let preference = cmpAttack.GetPreference(target); let weight = 1; if (preference !== null && preference !== undefined) weight += maxPreference / (1 + preference); targets.push(target, weight); }; // Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ. if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1) addTarget(this.unitAITarget); for (let target of this.targetUnits) addTarget(target); // The obstruction manager performs approximate range checks. // so we need to verify them here. // TODO: perhaps an optional 'precise' mode to range queries would be more performant. const cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); const range = cmpAttack.GetRange(attackType); const yOrigin = cmpAttack.GetAttackYOrigin(attackType); let firedArrows = 0; while (firedArrows < arrowsToFire && targets.length()) { const selectedTarget = targets.randomItem(); if (this.CheckTargetVisible(selectedTarget) && cmpObstructionManager.IsInTargetParabolicRange( this.entity, selectedTarget, range.min, range.max, yOrigin, false)) { cmpAttack.PerformAttack(attackType, selectedTarget); PlaySound("attack_" + attackType.toLowerCase(), this.entity); ++firedArrows; continue; } // Could not attack target, try a different target. targets.remove(selectedTarget); } this.arrowsLeft -= firedArrows; ++this.currentRound; }; /** * Returns true if the target entity is visible through the FoW/SoD. */ BuildingAI.prototype.CheckTargetVisible = function(target) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; // Entities that are hidden and miraged are considered visible. var cmpFogging = Engine.QueryInterface(target, IID_Fogging); if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner())) return true; // Either visible directly, or visible in fog. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) != "hidden"; }; Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI); Index: ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 27722) @@ -1,403 +1,403 @@ function Capturable() {} Capturable.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + ""; Capturable.prototype.Init = function() { this.maxCapturePoints = +this.template.CapturePoints; this.garrisonRegenRate = +this.template.GarrisonRegenRate; this.regenRate = +this.template.RegenRate; this.capturePoints = []; }; // Interface functions /** * Returns the current capture points array. */ Capturable.prototype.GetCapturePoints = function() { return this.capturePoints; }; Capturable.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; }; Capturable.prototype.GetGarrisonRegenRate = function() { return this.garrisonRegenRate; }; /** * Set the new capture points, used for cloning entities. * The caller should assure that the sum of capture points * matches the max. * @param {number[]} - Array with for all players the new value. */ Capturable.prototype.SetCapturePoints = function(capturePointsArray) { this.capturePoints = capturePointsArray; }; /** * Compute the amount of capture points to be reduced and reduce them. * @param {number} amount - Number of capture points to be taken. * @param {number} captor - The entity capturing us. * @param {number} captorOwner - Owner of the captor. * @return {Object} - Object of the form { "captureChange": number }, where number indicates the actual amount of capture points taken. */ Capturable.prototype.Capture = function(amount, captor, captorOwner) { if (captorOwner == INVALID_PLAYER || !this.CanCapture(captorOwner)) return {}; // TODO: implement loot return { "captureChange": this.Reduce(amount, captorOwner) }; }; /** * Reduces the amount of capture points of an entity, * in favour of the player of the source. * @param {number} amount - Number of capture points to be taken. * @param {number} playerID - ID of player the capture points should be awarded to. * @return {number} - The number of capture points actually taken. */ Capturable.prototype.Reduce = function(amount, playerID) { if (amount <= 0) return 0; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return 0; - let cmpPlayerSource = QueryPlayerIDInterface(playerID); - if (!cmpPlayerSource) + const cmpDiplomacySource = QueryPlayerIDInterface(playerID, IID_Diplomacy); + if (!cmpDiplomacySource) return 0; // Before changing the value, activate Fogging if necessary to hide changes. let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); - let numberOfEnemies = this.capturePoints.filter((v, i) => v > 0 && cmpPlayerSource.IsEnemy(i)).length; + let numberOfEnemies = this.capturePoints.filter((v, i) => v > 0 && cmpDiplomacySource.IsEnemy(i)).length; if (numberOfEnemies == 0) return 0; // Distribute the capture points over all enemies. let distributedAmount = amount / numberOfEnemies; let removedAmount = 0; while (distributedAmount > 0.0001) { numberOfEnemies = 0; for (let i in this.capturePoints) { - if (!this.capturePoints[i] || !cmpPlayerSource.IsEnemy(i)) + if (!this.capturePoints[i] || !cmpDiplomacySource.IsEnemy(i)) continue; if (this.capturePoints[i] > distributedAmount) { removedAmount += distributedAmount; this.capturePoints[i] -= distributedAmount; ++numberOfEnemies; } else { removedAmount += this.capturePoints[i]; this.capturePoints[i] = 0; } } distributedAmount = numberOfEnemies ? (amount - removedAmount) / numberOfEnemies : 0; } // Give all capture points taken to the player. let takenCapturePoints = this.maxCapturePoints - this.capturePoints.reduce((a, b) => a + b); this.capturePoints[playerID] += takenCapturePoints; this.CheckTimer(); this.RegisterCapturePointsChanged(); return takenCapturePoints; }; /** * Check if the source can (re)capture points from this building. * @param {number} playerID - PlayerID of the source. * @return {boolean} - Whether the source can (re)capture points from this building. */ Capturable.prototype.CanCapture = function(playerID) { - let cmpPlayerSource = QueryPlayerIDInterface(playerID); + const cmpDiplomacySource = QueryPlayerIDInterface(playerID, IID_Diplomacy); - if (!cmpPlayerSource) - warn(playerID + " has no player component defined on its id."); + if (!cmpDiplomacySource) + warn(playerID + " has no diplomacy component defined on its id."); let capturePoints = this.GetCapturePoints(); let sourceEnemyCapturePoints = 0; for (let i in this.GetCapturePoints()) - if (cmpPlayerSource.IsEnemy(i)) + if (cmpDiplomacySource.IsEnemy(i)) sourceEnemyCapturePoints += capturePoints[i]; return sourceEnemyCapturePoints > 0; }; // Private functions /** * This has to be called whenever the capture points are changed. * It notifies other components of the change, and switches ownership when needed. */ Capturable.prototype.RegisterCapturePointsChanged = function() { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return; Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints }); let owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER || this.capturePoints[owner] > 0) return; // If all capture points have been taken from the owner, convert it to player with the most capture points. let cmpLostPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpLostPlayerStatisticsTracker) cmpLostPlayerStatisticsTracker.LostEntity(this.entity); cmpOwnership.SetOwner(this.capturePoints.reduce((bestPlayer, playerCapturePoints, player, capturePoints) => playerCapturePoints > capturePoints[bestPlayer] ? player : bestPlayer, 0)); let cmpCapturedPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpCapturedPlayerStatisticsTracker) cmpCapturedPlayerStatisticsTracker.CapturedEntity(this.entity); }; Capturable.prototype.GetRegenRate = function() { const cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder) return this.regenRate; let total = this.regenRate; const garrisonRegenRate = this.GetGarrisonRegenRate(); for (const entity of cmpGarrisonHolder.GetEntities()) { const captureStrength = Engine.QueryInterface(entity, IID_Attack)?.GetAttackEffectsData("Capture")?.Capture; if (!captureStrength) continue; total += captureStrength * garrisonRegenRate; } return total; }; Capturable.prototype.TimerTick = function() { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return; let owner = cmpOwnership.GetOwner(); let modifiedCapturePoints = 0; // Special handle for the territory decay. // Reduce capture points from the owner in favour of all neighbours (also allies). let cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying()) { let neighbours = cmpTerritoryDecay.GetConnectedNeighbours(); let totalNeighbours = neighbours.reduce((a, b) => a + b); let decay = Math.min(cmpTerritoryDecay.GetDecayRate(), this.capturePoints[owner]); this.capturePoints[owner] -= decay; if (totalNeighbours) for (let p in neighbours) this.capturePoints[p] += decay * neighbours[p] / totalNeighbours; // Decay to gaia as default. else this.capturePoints[0] += decay; modifiedCapturePoints += decay; this.RegisterCapturePointsChanged(); } let regenRate = this.GetRegenRate(); if (regenRate < 0) modifiedCapturePoints += this.Reduce(-regenRate, 0); else if (regenRate > 0) modifiedCapturePoints += this.Reduce(regenRate, owner); if (modifiedCapturePoints) return; // Nothing changed, stop the timer. let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); delete this.timer; Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": false, "regenRate": 0, "territoryDecay": 0 }); }; /** * Start the regeneration timer when no timer exists. * When nothing can be modified (f.e. because it is fully regenerated), the * timer stops automatically after one execution. */ Capturable.prototype.CheckTimer = function() { if (this.timer) return; let regenRate = this.GetRegenRate(); let cmpDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); let decay = cmpDecay && cmpDecay.IsDecaying() ? cmpDecay.GetDecayRate() : 0; if (regenRate == 0 && decay == 0) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Capturable, "TimerTick", 1000, 1000, null); Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": true, "regenRate": regenRate, "territoryDecay": decay }); }; /** * Update all chached values that could be affected by modifications. */ Capturable.prototype.UpdateCachedValues = function() { this.garrisonRegenRate = ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity); this.regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", +this.template.RegenRate, this.entity); this.maxCapturePoints = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity); }; /** * Update all chached values that could be affected by modifications. * Check timer and send changed messages when required. * @param {boolean} message - Whether not to send a CapturePointsChanged message. When false, caller should take care of sending that message. */ Capturable.prototype.UpdateCachedValuesAndNotify = function(sendMessage = true) { let oldMaxCapturePoints = this.maxCapturePoints; let oldGarrisonRegenRate = this.garrisonRegenRate; let oldRegenRate = this.regenRate; this.UpdateCachedValues(); if (oldMaxCapturePoints != this.maxCapturePoints) { let scale = this.maxCapturePoints / oldMaxCapturePoints; for (let i in this.capturePoints) this.capturePoints[i] *= scale; if (sendMessage) Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints }); } if (oldGarrisonRegenRate != this.garrisonRegenRate || oldRegenRate != this.regenRate) this.CheckTimer(); }; // Message Listeners Capturable.prototype.OnValueModification = function(msg) { if (msg.component == "Capturable") this.UpdateCachedValuesAndNotify(); }; Capturable.prototype.OnGarrisonedUnitsChanged = function(msg) { this.CheckTimer(); }; Capturable.prototype.OnTerritoryDecayChanged = function(msg) { if (msg.to) this.CheckTimer(); }; Capturable.prototype.OnDiplomacyChanged = function(msg) { this.CheckTimer(); }; Capturable.prototype.OnOwnershipChanged = function(msg) { if (msg.to == INVALID_PLAYER) return; // Initialise the capture points when created. if (!this.capturePoints.length) { this.UpdateCachedValues(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { if (i == msg.to) this.capturePoints[i] = this.maxCapturePoints; else this.capturePoints[i] = 0; } this.CheckTimer(); return; } // When already initialised, this happens on defeat or wololo, // transfer the points of the old owner to the new one. if (this.capturePoints[msg.from]) { this.capturePoints[msg.to] += this.capturePoints[msg.from]; this.capturePoints[msg.from] = 0; this.UpdateCachedValuesAndNotify(false); this.RegisterCapturePointsChanged(); return; } this.UpdateCachedValuesAndNotify(); }; /** * When a player is defeated, reassign the capture points of non-owned entities to gaia. * Those owned by the defeated player are dealt with onOwnershipChanged. */ Capturable.prototype.OnGlobalPlayerDefeated = function(msg) { if (!this.capturePoints[msg.playerId]) return; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && (cmpOwnership.GetOwner() == INVALID_PLAYER || cmpOwnership.GetOwner() == msg.playerId)) return; this.capturePoints[0] += this.capturePoints[msg.playerId]; this.capturePoints[msg.playerId] = 0; this.RegisterCapturePointsChanged(); this.CheckTimer(); }; function CapturableMirage() {} CapturableMirage.prototype.Init = function(cmpCapturable) { this.capturePoints = clone(cmpCapturable.GetCapturePoints()); this.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); }; CapturableMirage.prototype.GetCapturePoints = function() { return this.capturePoints; }; CapturableMirage.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; }; CapturableMirage.prototype.CanCapture = Capturable.prototype.CanCapture; Engine.RegisterGlobal("CapturableMirage", CapturableMirage); Capturable.prototype.Mirage = function() { let mirage = new CapturableMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable); Index: ps/trunk/binaries/data/mods/public/simulation/components/CeasefireManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/CeasefireManager.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/CeasefireManager.js (revision 27722) @@ -1,134 +1,134 @@ function CeasefireManager() {} CeasefireManager.prototype.Schema = ""; CeasefireManager.prototype.Init = function() { // Weather or not ceasefire is active currently. this.ceasefireIsActive = false; // Ceasefire timeout in milliseconds this.ceasefireTime = 0; // Time elapsed when the ceasefire was started this.ceasefireStartedTime = 0; // diplomacy states before the ceasefire started this.diplomacyBeforeCeasefire = []; // Message duration for the countdown in milliseconds this.countdownMessageDuration = 10000; // Duration for the post ceasefire message in milliseconds this.postCountdownMessageDuration = 5000; }; CeasefireManager.prototype.IsCeasefireActive = function() { return this.ceasefireIsActive; }; CeasefireManager.prototype.GetCeasefireStartedTime = function() { return this.ceasefireStartedTime; }; CeasefireManager.prototype.GetCeasefireTime = function() { return this.ceasefireTime; }; CeasefireManager.prototype.GetDiplomacyBeforeCeasefire = function() { return this.diplomacyBeforeCeasefire; }; CeasefireManager.prototype.StartCeasefire = function(ceasefireTime) { // If invalid timeout given, return if (ceasefireTime <= 0) return; // Remove existing timers let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (this.ceasefireCountdownMessageTimer) cmpTimer.CancelTimer(this.ceasefireCountdownMessageTimer); if (this.stopCeasefireTimer) cmpTimer.CancelTimer(this.stopCeasefireTimer); // Remove existing messages let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (this.ceasefireCountdownMessage) cmpGuiInterface.DeleteTimeNotification(this.ceasefireCountdownMessage); if (this.ceasefireEndedMessage) cmpGuiInterface.DeleteTimeNotification(this.ceasefireEndedMessage); // Save diplomacy and set everyone neutral if (!this.ceasefireIsActive) { // Save diplomacy let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) - this.diplomacyBeforeCeasefire.push(QueryPlayerIDInterface(i).GetDiplomacy()); + this.diplomacyBeforeCeasefire.push(QueryPlayerIDInterface(i, IID_Diplomacy).GetDiplomacy()); // Set every enemy (except gaia) to neutral for (let i = 1; i < numPlayers; ++i) for (let j = 1; j < numPlayers; ++j) if (this.diplomacyBeforeCeasefire[i-1][j] < 0) - QueryPlayerIDInterface(i).SetNeutral(j); + QueryPlayerIDInterface(i, IID_Diplomacy).SetNeutral(j); } this.ceasefireIsActive = true; this.ceasefireTime = ceasefireTime; this.ceasefireStartedTime = cmpTimer.GetTime(); Engine.PostMessage(SYSTEM_ENTITY, MT_CeasefireStarted); // Add timers for countdown message and resetting diplomacy this.stopCeasefireTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_CeasefireManager, "StopCeasefire", this.ceasefireTime); this.ceasefireCountdownMessageTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_CeasefireManager, "ShowCeasefireCountdownMessage", this.ceasefireTime - this.countdownMessageDuration); }; CeasefireManager.prototype.ShowCeasefireCountdownMessage = function() { let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); this.ceasefireCountdownMessage = cmpGuiInterface.AddTimeNotification({ "message": markForTranslation("You can attack in %(time)s"), "translateMessage": true }, this.countdownMessageDuration); }; CeasefireManager.prototype.StopCeasefire = function() { let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (this.ceasefireCountdownMessage) cmpGuiInterface.DeleteTimeNotification(this.ceasefireCountdownMessage); this.ceasefireEndedMessage = cmpGuiInterface.AddTimeNotification({ "message": markForTranslation("You can attack now!"), "translateMessage": true }, this.postCountdownMessageDuration); // Reset diplomacies to original settings let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) - QueryPlayerIDInterface(i).SetDiplomacy(this.diplomacyBeforeCeasefire[i-1]); + QueryPlayerIDInterface(i, IID_Diplomacy).SetDiplomacy(this.diplomacyBeforeCeasefire[i-1]); this.ceasefireIsActive = false; this.ceasefireTime = 0; this.ceasefireStartedTime = 0; this.diplomacyBeforeCeasefire = []; Engine.PostMessage(SYSTEM_ENTITY, MT_CeasefireEnded); cmpGuiInterface.PushNotification({ "type": "ceasefire-ended", "players": [-1] // processed globally }); }; Engine.RegisterSystemComponentType(IID_CeasefireManager, "CeasefireManager", CeasefireManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/Diplomacy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Diplomacy.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/Diplomacy.js (revision 27722) @@ -0,0 +1,347 @@ +function Diplomacy() {} + +Diplomacy.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + ""; + +Diplomacy.prototype.SerializableAttributes = [ + "team", + "teamLocked", + "diplomacy", + "sharedDropsites", +]; + +Diplomacy.prototype.Serialize = function() +{ + const state = {}; + for (const key in this.SerializableAttributes) + if (this.hasOwnProperty(key)) + state[key] = this[key]; + + return state; +}; + +Diplomacy.prototype.Deserialize = function(state) +{ + for (const prop in state) + this[prop] = state[prop]; +}; + +Diplomacy.prototype.Init = function() +{ + // 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.team = -1; + + // Array of diplomatic stances for this player with respect to other players (including gaia and self). + this.diplomacy = []; +}; + +/** + * @param {Object} color - r, g, b values of the diplomacy colour. + */ +Diplomacy.prototype.SetDiplomacyColor = function(color) +{ + this.diplomacyColor = { "r": color.r / 255, "g": color.g / 255, "b": color.b / 255, "a": 1 }; +}; + +/** + * @return {Object} - + */ +Diplomacy.prototype.GetColor = function() +{ + return this.diplomacyColor; +}; + +/** + * @return {number} - + */ +Diplomacy.prototype.GetTeam = function() +{ + return this.team; +}; + +/** + * @param {number} team - The new team number, -1 for no team. + */ +Diplomacy.prototype.ChangeTeam = function(team) +{ + if (this.teamLocked || this.team === team) + return; + + const playerID = Engine.QueryInterface(this.entity, IID_Player)?.GetPlayerID(); + if (!playerID) + return; + + // ToDo: Fix this. + if (this.team !== -1) + warn("A change in teams is requested while the player already had a team, previous alliances are maintained."); + + this.team = team; + + if (this.team !== -1) + { + const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + for (let i = 0; i < numPlayers; ++i) + { + const cmpDiplomacy = QueryPlayerIDInterface(i, IID_Diplomacy); + if (this.team !== cmpDiplomacy.GetTeam()) + continue; + + this.Ally(i); + cmpDiplomacy.Ally(playerID); + } + } + + Engine.BroadcastMessage(MT_DiplomacyChanged, { + "player": playerID, + "otherPlayer": null + }); +}; + +Diplomacy.prototype.LockTeam = function() +{ + this.teamLocked = true; +}; + +Diplomacy.prototype.UnLockTeam = function() +{ + delete this.teamLocked; +}; + +/** + * @return {boolean} - + */ +Diplomacy.prototype.IsTeamLocked = function() +{ + return !!this.teamLocked; +}; + +/** + * @return {number[]} - Current diplomatic stances. + */ +Diplomacy.prototype.GetDiplomacy = function() +{ + return this.diplomacy.slice(); +}; + +/** + * @param {number[]} dipl - The diplomacy array to set. + */ +Diplomacy.prototype.SetDiplomacy = function(dipl) +{ + const playerID = Engine.QueryInterface(this.entity, IID_Player)?.GetPlayerID(); + if (!playerID) + return + + this.diplomacy = dipl.slice(); + + Engine.BroadcastMessage(MT_DiplomacyChanged, { + "player": playerID, + "otherPlayer": null + }); +}; + +/** + * Helper function for allying etc. + * @param {number} idx - The player number. + * @param {number} value - The diplomacy value. + */ +Diplomacy.prototype.SetDiplomacyIndex = function(idx, value) +{ + if (!QueryPlayerIDInterface(idx)?.IsActive()) + return; + + const cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + if (!cmpPlayer?.IsActive()) + return; + + this.diplomacy[idx] = value; + + const playerID = cmpPlayer.GetPlayerID(); + Engine.BroadcastMessage(MT_DiplomacyChanged, { + "player": playerID, + "otherPlayer": idx, + "value": value + }); +}; + +/** + * Helper function for getting allies etc. + * @param {string} func - Name of the function to test. + * @return {number[]} - Player IDs matching the function. + */ +Diplomacy.prototype.GetPlayersByDiplomacy = function(func) +{ + const players = []; + for (let i = 0; i < this.diplomacy.length; ++i) + if (this[func](i)) + players.push(i); + return players; +}; + +/** + * @param {number} - id + */ +Diplomacy.prototype.Ally = function(id) +{ + this.SetDiplomacyIndex(id, 1); +}; + +/** + * Check if given player is our ally. + * @param {number} id - + * @return {boolean} - + */ +Diplomacy.prototype.IsAlly = function(id) +{ + return this.diplomacy[id] > 0; +}; + +/** + * @return {number[]} - + */ +Diplomacy.prototype.GetAllies = function() +{ + return this.GetPlayersByDiplomacy("IsAlly"); +}; + +/** + * Check if given player is our ally excluding ourself. + * @param {number} id - + * @return {boolean} - + */ +Diplomacy.prototype.IsExclusiveAlly = function(id) +{ + return Engine.QueryInterface(this.entity, IID_Player)?.GetPlayerID() !== id && this.IsAlly(id); +}; + +/** + * Check if given player is our ally, and we are its ally. + * @param {number} id - + * @return {boolean} - + */ +Diplomacy.prototype.IsMutualAlly = function(id) +{ + const playerID = Engine.QueryInterface(this.entity, IID_Player)?.GetPlayerID(); + return this.IsAlly(id) && playerID && QueryPlayerIDInterface(id, IID_Diplomacy)?.IsAlly(playerID); +}; + +/** + * @return {number[]} - + */ +Diplomacy.prototype.GetMutualAllies = function() +{ + return this.GetPlayersByDiplomacy("IsMutualAlly"); +}; + +/** + * Check if given player is our ally, and we are its ally, excluding ourself. + * @param {number} id - + * @return {boolean} - + */ +Diplomacy.prototype.IsExclusiveMutualAlly = function(id) +{ + const playerID = Engine.QueryInterface(this.entity, IID_Player)?.GetPlayerID(); + return playerID && playerID !== id && this.IsMutualAlly(id); +}; + +/** + * @param {number} id - + */ +Diplomacy.prototype.SetEnemy = function(id) +{ + this.SetDiplomacyIndex(id, -1); +}; + +/** + * Check if given player is our enemy. + * @param {number} id - + * @return {boolean} - + */ +Diplomacy.prototype.IsEnemy = function(id) +{ + return this.diplomacy[id] < 0; +}; + +/** + * @return {number[]} - + */ +Diplomacy.prototype.GetEnemies = function() +{ + return this.GetPlayersByDiplomacy("IsEnemy"); +}; + +/** + * @param {number} id - + */ +Diplomacy.prototype.SetNeutral = function(id) +{ + this.SetDiplomacyIndex(id, 0); +}; + +/** + * Check if given player is neutral. + * @param {number} id - + * @return {boolean} - + */ +Diplomacy.prototype.IsNeutral = function(id) +{ + return this.diplomacy[id] === 0; +}; + +/** + * @return {boolean} - + */ +Diplomacy.prototype.HasSharedDropsites = function() +{ + return this.sharedDropsites; +}; + +/** + * @return {boolean} - + */ +Diplomacy.prototype.HasSharedLos = function() +{ + const cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); + return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech); +}; + +Diplomacy.prototype.UpdateSharedLos = function() +{ + const playerID = Engine.QueryInterface(this.entity, IID_Player).GetPlayerID(); + if (!playerID) + return; + + Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager)?. + SetSharedLos(playerID, this.HasSharedLos() ? this.GetMutualAllies() : [playerID]); +}; + +Diplomacy.prototype.OnResearchFinished = function(msg) +{ + if (msg.tech === this.template.SharedLosTech) + this.UpdateSharedLos(); + else if (msg.tech === this.template.SharedDropsitesTech) + this.sharedDropsites = true; +}; + +Diplomacy.prototype.OnDiplomacyChanged = function(msg) +{ + this.UpdateSharedLos(); + + if (msg.otherPlayer === null) + return; + + const cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + if (!cmpPlayer || cmpPlayer.GetPlayerID() != msg.otherPlayer) + return; + + // Mutual worsening of relations. + if (this.diplomacy[msg.player] > msg.value) + this.SetDiplomacyIndex(msg.player, msg.value); +}; + +Engine.RegisterComponentType(IID_Diplomacy, "Diplomacy", Diplomacy); Index: ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js (revision 27722) @@ -1,197 +1,197 @@ /** * System component to store the victory conditions and their settings and * check for allied victory / last-man-standing. */ function EndGameManager() {} EndGameManager.prototype.Schema = ""; EndGameManager.prototype.Init = function() { // Contains settings specific to the victory condition, // for example wonder victory duration. this.gameSettings = {}; // Allied victory means allied players can win if victory conditions are met for each of them // False for a "last man standing" game this.alliedVictory = true; // Don't do any checks before the diplomacies were set for each player // or when marking a player as won. this.skipAlliedVictoryCheck = true; this.lastManStandingMessage = undefined; this.endlessGame = false; }; EndGameManager.prototype.GetGameSettings = function() { return this.gameSettings; }; EndGameManager.prototype.GetVictoryConditions = function() { return this.gameSettings.victoryConditions; }; EndGameManager.prototype.SetGameSettings = function(newSettings = {}) { this.gameSettings = newSettings; this.skipAlliedVictoryCheck = false; this.endlessGame = !this.gameSettings.victoryConditions.length; Engine.BroadcastMessage(MT_VictoryConditionsChanged, {}); }; /** * Sets the given player (and the allies if allied victory is enabled) as a winner. * * @param {number} playerID - The player that should win. * @param {function} victoryReason - Function that maps from number to plural string, for example * n => markForPluralTranslation( * "%(lastPlayer)s has won (game mode).", * "%(players)s and %(lastPlayer)s have won (game mode).", * n)); */ EndGameManager.prototype.MarkPlayerAndAlliesAsWon = function(playerID, victoryString, defeatString) { const cmpPlayer = QueryPlayerIDInterface(playerID); if (!cmpPlayer.IsActive()) { warn("Can't mark player " + playerID + " as won, since the state is " + cmpPlayer.GetState()); return; } let winningPlayers = [playerID]; if (this.alliedVictory) - winningPlayers = cmpPlayer.GetMutualAllies(playerID).filter( + winningPlayers = QueryPlayerIDInterface(playerID, IID_Diplomacy).GetMutualAllies(playerID).filter( player => QueryPlayerIDInterface(player).IsActive()); this.MarkPlayersAsWon(winningPlayers, victoryString, defeatString); }; /** * Sets the given players as won and others as defeated. * * @param {array} winningPlayers - The players that should win. * @param {function} victoryReason - Function that maps from number to plural string, for example * n => markForPluralTranslation( * "%(lastPlayer)s has won (game mode).", * "%(players)s and %(lastPlayer)s have won (game mode).", * n)); */ EndGameManager.prototype.MarkPlayersAsWon = function(winningPlayers, victoryString, defeatString) { this.skipAlliedVictoryCheck = true; for (let playerID of winningPlayers) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (!cmpPlayer.IsActive()) { warn("Can't mark player " + playerID + " as won, since the state is " + cmpPlayer.GetState()); continue; } cmpPlayer.Win(undefined); } let defeatedPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers().filter( playerID => winningPlayers.indexOf(playerID) == -1); for (let playerID of defeatedPlayers) QueryPlayerIDInterface(playerID).Defeat(undefined); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "won", "players": [winningPlayers[0]], "allies": winningPlayers, "message": victoryString(winningPlayers.length) }); if (defeatedPlayers.length) cmpGUIInterface.PushNotification({ "type": "defeat", "players": [defeatedPlayers[0]], "allies": defeatedPlayers, "message": defeatString(defeatedPlayers.length) }); this.skipAlliedVictoryCheck = false; }; EndGameManager.prototype.SetAlliedVictory = function(flag) { this.alliedVictory = flag; }; EndGameManager.prototype.GetAlliedVictory = function() { return this.alliedVictory; }; EndGameManager.prototype.AlliedVictoryCheck = function() { if (this.skipAlliedVictoryCheck || this.endlessGame) return; let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.DeleteTimeNotification(this.lastManStandingMessage); // Proceed if only allies are remaining let allies = []; let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let playerID = 1; playerID < numPlayers; ++playerID) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (!cmpPlayer.IsActive()) continue; - if (allies.length && !cmpPlayer.IsMutualAlly(allies[0])) + if (allies.length && !QueryPlayerIDInterface(playerID, IID_Diplomacy).IsMutualAlly(allies[0])) return; allies.push(playerID); } if (!allies.length) return; if (this.alliedVictory || allies.length == 1) { for (const playerID of allies) QueryPlayerIDInterface(playerID)?.Win(undefined); cmpGuiInterface.PushNotification({ "type": "won", "players": [allies[0]], "allies": allies, "message": markForPluralTranslation( "%(lastPlayer)s has won (last player alive).", "%(players)s and %(lastPlayer)s have won (last players alive).", allies.length) }); } else this.lastManStandingMessage = cmpGuiInterface.AddTimeNotification({ "message": markForTranslation("Last remaining player wins."), "translateMessage": true, }, 12 * 60 * 60 * 1000); // 12 hours }; EndGameManager.prototype.OnInitGame = function(msg) { this.AlliedVictoryCheck(); }; EndGameManager.prototype.OnGlobalDiplomacyChanged = function(msg) { this.AlliedVictoryCheck(); }; EndGameManager.prototype.OnGlobalPlayerDefeated = function(msg) { this.AlliedVictoryCheck(); }; Engine.RegisterSystemComponentType(IID_EndGameManager, "EndGameManager", EndGameManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/Gate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Gate.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/Gate.js (revision 27722) @@ -1,285 +1,285 @@ function Gate() {} Gate.prototype.Schema = "Controls behavior of wall gates" + "" + "20" + "" + "" + "" + ""; /** * Initialize Gate component */ Gate.prototype.Init = function() { this.allies = []; this.ignoreList = []; this.opened = false; this.locked = false; }; Gate.prototype.OnOwnershipChanged = function(msg) { if (msg.to != INVALID_PLAYER) { this.SetupRangeQuery(msg.to); // Set the initial state, but don't play unlocking sound if (!this.locked) this.UnlockGate(true); } }; Gate.prototype.OnDiplomacyChanged = function(msg) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) { this.allies = []; this.ignoreList = []; this.SetupRangeQuery(msg.player); } }; /** * Cleanup on destroy */ Gate.prototype.OnDestroy = function() { // Clean up range query var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.unitsQuery) cmpRangeManager.DestroyActiveQuery(this.unitsQuery); // Cancel the closing-blocked timer if it's running. if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } }; /** * Setup the range query to detect units coming in & out of range */ Gate.prototype.SetupRangeQuery = function(owner) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.unitsQuery) cmpRangeManager.DestroyActiveQuery(this.unitsQuery); // Only allied units can make the gate open. - var players = QueryPlayerIDInterface(owner).GetAllies(); + const players = QueryPlayerIDInterface(owner, IID_Diplomacy).GetAllies(); var range = this.GetPassRange(); if (range > 0) { // Only find entities with IID_UnitAI interface this.unitsQuery = cmpRangeManager.CreateActiveQuery(this.entity, 0, range, players, IID_UnitAI, cmpRangeManager.GetEntityFlagMask("normal"), true); cmpRangeManager.EnableActiveQuery(this.unitsQuery); } }; /** * Called when units enter or leave range */ Gate.prototype.OnRangeUpdate = function(msg) { if (msg.tag != this.unitsQuery) return; if (msg.added.length > 0) for (let entity of msg.added) { // Ignore entities that cannot move as those won't be able to go through the gate. let unitAI = Engine.QueryInterface(entity, IID_UnitAI); if (!unitAI || !unitAI.AbleToMove()) this.ignoreList.push(entity); this.allies.push(entity); } if (msg.removed.length > 0) for (let entity of msg.removed) { let index = this.ignoreList.indexOf(entity); if (index !== -1) this.ignoreList.splice(index, 1); this.allies.splice(this.allies.indexOf(entity), 1); } this.OperateGate(); }; Gate.prototype.OnGlobalUnitAbleToMoveChanged = function(msg) { if (this.allies.indexOf(msg.entity) === -1) return; let index = this.ignoreList.indexOf(msg.entity); if (msg.ableToMove && index !== -1) this.ignoreList.splice(index, 1); else if (!msg.ableToMove && index === -1) this.ignoreList.push(msg.entity); this.OperateGate(); }; /** * Get the range in which units are detected */ Gate.prototype.GetPassRange = function() { return +this.template.PassRange; }; Gate.prototype.ShouldOpen = function() { return this.allies.some(ent => this.ignoreList.indexOf(ent) === -1); }; /** * Attempt to open or close the gate. * An ally must be in range to open the gate, but an unlocked gate will only close * if there are no allies in range and no units are inside the gate's obstruction. */ Gate.prototype.OperateGate = function() { // Cancel the closing-blocked timer if it's running. if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } if (this.opened && (this.locked || !this.ShouldOpen())) this.CloseGate(); else if (!this.opened && this.ShouldOpen()) this.OpenGate(); }; Gate.prototype.IsLocked = function() { return this.locked; }; /** * Lock the gate, with sound. It will close at the next opportunity. */ Gate.prototype.LockGate = function() { this.locked = true; // Delete animal corpses to prevent units trying to gather the unreachable entity let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true)) for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction()) Engine.DestroyEntity(ent); // If the door is closed, enable 'block pathfinding' // Else 'block pathfinding' will be enabled the next time the gate close if (!this.opened) { if (cmpObstruction) cmpObstruction.SetDisableBlockMovementPathfinding(false, false, 0); } else this.OperateGate(); // TODO: Possibly move the lock/unlock sounds to UI? Needs testing PlaySound("gate_locked", this.entity); }; /** * Unlock the gate, with sound. May open the gate if allied units are within range. * If quiet is true, no sound will be played (used for initial setup). */ Gate.prototype.UnlockGate = function(quiet) { var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return; // Disable 'block pathfinding' cmpObstruction.SetDisableBlockMovementPathfinding(this.opened, true, 0); this.locked = false; // TODO: Possibly move the lock/unlock sounds to UI? Needs testing if (!quiet) PlaySound("gate_unlocked", this.entity); // If the gate is closed, open it if necessary if (!this.opened) this.OperateGate(); }; /** * Open the gate if unlocked, with sound and animation. */ Gate.prototype.OpenGate = function() { // Do not open the gate if it has been locked if (this.locked) return; var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return; // Disable 'block movement' cmpObstruction.SetDisableBlockMovementPathfinding(true, true, 0); this.opened = true; PlaySound("gate_opening", this.entity); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("gate_opening", true, 1.0); }; /** * Close the gate, with sound and animation. * * The gate may fail to close due to unit obstruction. If this occurs, the * gate will start a timer and attempt to close on each simulation update. */ Gate.prototype.CloseGate = function() { let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return; // The gate can't be closed if there are entities colliding with it. // NB: because walls are overlapping, they requires special care to not break // in particular, walls do not block construction, so walls from skirmish maps // do not appear in this check even if they have different control groups from the gate. // This no longer works if gates are made to check for entities blocking movement. // Fixing that would let us change this code, but it sounds decidedly non-trivial. let collisions = cmpObstruction.GetEntitiesBlockingConstruction(); if (collisions.length) { if (!this.timer) { // Set an "instant" timer which will run on the next simulation turn. let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_Gate, "OperateGate", 0); } return; } // If we ordered the gate to be locked, enable 'block movement' and 'block pathfinding' if (this.locked) cmpObstruction.SetDisableBlockMovementPathfinding(false, false, 0); // Else just enable 'block movement' else cmpObstruction.SetDisableBlockMovementPathfinding(false, true, 0); this.opened = false; PlaySound("gate_closing", this.entity); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("gate_closing", true, 1.0); }; Engine.RegisterComponentType(IID_Gate, "Gate", Gate); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 27722) @@ -1,2135 +1,2135 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; +GuiInterface.prototype.WHITE = { "r": 1, "g": 1, "b": 1 }; + GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronized for the biggest part, // so most of the attributes shouldn't be serialized. // Return an object with a small selection of deterministic data. return { "timeNotifications": this.timeNotifications, "timeNotificationID": this.timeNotificationID }; }; GuiInterface.prototype.Deserialize = function(data) { this.Init(); this.timeNotifications = data.timeNotifications; this.timeNotificationID = data.timeNotificationID; }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.miragedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; this.templateModified = {}; this.selectionDirty = {}; this.obstructionSnap = new ObstructionSnap(); }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function() { let ret = { "players": [] }; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let numPlayers = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { const playerEnt = cmpPlayerManager.GetPlayerByID(i); const cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); const cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); const cmpIdentity = Engine.QueryInterface(playerEnt, IID_Identity); + const cmpDiplomacy = Engine.QueryInterface(playerEnt, IID_Diplomacy); // Work out which phase we are in. let phase = ""; const cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (cmpTechnologyManager) { if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; } let allies = []; let mutualAllies = []; let neutrals = []; let enemies = []; for (let j = 0; j < numPlayers; ++j) { - allies[j] = cmpPlayer.IsAlly(j); - mutualAllies[j] = cmpPlayer.IsMutualAlly(j); - neutrals[j] = cmpPlayer.IsNeutral(j); - enemies[j] = cmpPlayer.IsEnemy(j); + allies[j] = cmpDiplomacy.IsAlly(j); + mutualAllies[j] = cmpDiplomacy.IsMutualAlly(j); + neutrals[j] = cmpDiplomacy.IsNeutral(j); + enemies[j] = cmpDiplomacy.IsEnemy(j); } ret.players.push({ "name": cmpIdentity.GetName(), "civ": cmpIdentity.GetCiv(), "color": cmpPlayer.GetColor(), "entity": cmpPlayer.entity, "controlsAll": cmpPlayer.CanControlAllUnits(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "panelEntities": cmpPlayer.GetPanelEntities(), "resourceCounts": cmpPlayer.GetResourceCounts(), "resourceGatherers": cmpPlayer.GetResourceGatherers(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), - "team": cmpPlayer.GetTeam(), - "teamsLocked": cmpPlayer.GetLockTeams(), + "team": cmpDiplomacy.GetTeam(), + "teamLocked": cmpDiplomacy.IsTeamLocked(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(), - "hasSharedDropsites": cmpPlayer.HasSharedDropsites(), - "hasSharedLos": cmpPlayer.HasSharedLos(), + "hasSharedDropsites": cmpDiplomacy.HasSharedDropsites(), + "hasSharedLos": cmpDiplomacy.HasSharedLos(), "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null, "canBarter": cmpPlayer.CanBarter(), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer) }); } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = cmpTerrain.GetMapSize(); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (cmpCeasefireManager) { ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive(); ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0; } let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics(); } return ret; }; /** * Returns global information about the current game state, plus statistics. * This is used by the GUI at the end of a game, in the summary screen. * Note: Amongst statistics, the team exploration map percentage is computed from * scratch, so the extended simulation state should not be requested too often. */ GuiInterface.prototype.GetExtendedSimulationState = function() { let ret = this.GetSimulationState(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } return ret; }; /** * Returns the gamesettings that were chosen at the time the match started. */ GuiInterface.prototype.GetInitAttributes = function() { return InitAttributes; }; /** * This data will be stored in the replay metadata file after a match has been finished recording. */ GuiInterface.prototype.GetReplayMetadata = function() { let extendedSimState = this.GetExtendedSimulationState(); return { "timeElapsed": extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "mapSettings": InitAttributes.settings }; }; /** * Called when the game ends if the current game is part of a campaign run. */ GuiInterface.prototype.GetCampaignGameEndData = function(player) { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (Trigger.prototype.OnCampaignGameEnd) return Trigger.prototype.OnCampaignGameEnd(); return {}; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function() { this.renamedEntities = []; this.miragedEntities = []; }; GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage) { if (!this.miragedEntities[player]) this.miragedEntities[player] = []; this.miragedEntities[player].push({ "entity": entity, "newentity": mirage }); }; /** * Get common entity info, often used in the gui. */ GuiInterface.prototype.GetEntityState = function(player, ent) { if (!ent) return null; // All units must have a template; if not then it's a nonexistent entity id. const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(ent); if (!template) return null; const ret = { "id": ent, "player": INVALID_PLAYER, "template": template }; const cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras) ret.auras = cmpAuras.GetDescriptions(); let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) ret.identity = { "rank": cmpIdentity.GetRank(), "rankTechName": cmpIdentity.GetRankTechName(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable(), "controllable": cmpIdentity.IsControllable() }; const cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) ret.formation = { "members": cmpFormation.GetMembers() }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) ret.position = cmpPosition.GetPosition(); let cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured(); ret.needsHeal = !cmpHealth.IsUnhealable(); } let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable) { ret.capturePoints = cmpCapturable.GetCapturePoints(); ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); } let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; let cmpMarket = QueryMiragedInterface(ent, IID_Market); if (cmpMarket) ret.market = { "land": cmpMarket.HasType("land"), "naval": cmpMarket.HasType("naval") }; let cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress() }; let cmpPopulation = Engine.QueryInterface(ent, IID_Population); if (cmpPopulation) ret.population = { "bonus": cmpPopulation.GetPopBonus() }; let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades": cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo(), "isUpgrading": cmpUpgrade.IsUpgrading() }; const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher); if (cmpResearcher) ret.researcher = { "technologies": cmpResearcher.GetTechnologiesList(), "techCostMultiplier": cmpResearcher.GetTechCostMultiplier() }; let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver); if (cmpStatusEffects) ret.statusEffects = cmpStatusEffects.GetActiveStatuses(); let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "queue": cmpProductionQueue.GetQueue(), "autoqueue": cmpProductionQueue.IsAutoQueueing() }; const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer); if (cmpTrainer) ret.trainer = { "entities": cmpTrainer.GetEntitiesList() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) ret.foundation = { "numBuilders": cmpFoundation.GetNumBuilders(), "buildTime": cmpFoundation.GetBuildTime() }; let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders(), "buildTime": cmpRepairable.GetBuildTime() }; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) ret.player = cmpOwnership.GetOwner(); let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "occupiedSlots": cmpGarrisonHolder.OccupiedSlots() }; let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); if (cmpTurretHolder) ret.turretHolder = { "turretPoints": cmpTurretHolder.GetTurretPoints() }; let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (cmpTurretable) ret.turretable = { "ejectable": cmpTurretable.IsEjectable(), "holder": cmpTurretable.HolderID() }; let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) ret.garrisonable = { "holder": cmpGarrisonable.HolderID(), "size": cmpGarrisonable.UnitSize() }; let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), "canPatrol": cmpUnitAI.CanPatrol(), "selectableStances": cmpUnitAI.GetSelectableStances(), "isIdle": cmpUnitAI.IsIdle(), "formations": cmpUnitAI.GetFormationsList(), "formation": cmpUnitAI.GetFormationController() }; let cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) ret.guard = { "entities": cmpGuard.GetEntities() }; let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } let cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) ret.gate = { "locked": cmpGate.IsLocked() }; let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) ret.alertRaiser = true; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); let cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { let types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (let type of types) { ret.attack[type] = {}; Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type)); ret.attack[type].attackName = cmpAttack.GetAttackName(type); ret.attack[type].splash = cmpAttack.GetSplashData(type); if (ret.attack[type].splash) Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true)); let range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; ret.attack[type].yOrigin = cmpAttack.GetAttackYOrigin(type); let timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } if (cmpPosition && cmpPosition.IsInWorld()) // For units, take the range in front of it, no spread, so angle = 0, // else, take the average elevation around it: angle = 2 * pi. ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, ret.attack[type].yOrigin, cmpUnitAI ? 0 : 2 * Math.PI); else // Not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } let cmpResistance = QueryMiragedInterface(ent, IID_Resistance); if (cmpResistance) ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity"); let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "numGatherers": cmpResourceSupply.GetNumGatherers() }; let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes(), "sharable": cmpResourceDropsite.IsSharable(), "shared": cmpResourceDropsite.IsShared() }; let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "health": cmpHeal.GetHealth(), "range": cmpHeal.GetRange().max, "interval": cmpHeal.GetInterval(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses() }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { ret.loot = cmpLoot.GetResources(); ret.loot.xp = cmpLoot.GetXp(); } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) ret.resourceTrickle = { "interval": cmpResourceTrickle.GetInterval(), "rates": cmpResourceTrickle.GetRates() }; let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure); if (cmpTreasure) ret.treasure = { "collectTime": cmpTreasure.CollectionTime(), "resources": cmpTreasure.Resources() }; let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector); if (cmpTreasureCollector) ret.treasureCollector = true; let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(), "acceleration": cmpUnitMotion.GetAcceleration() }; let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep); if (cmpUpkeep) ret.upkeep = { "interval": cmpUpkeep.GetInterval(), "rates": cmpUpkeep.GetRates() }; return ret; }; GuiInterface.prototype.GetMultipleEntityStates = function(player, ents) { return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) })); }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); let rot = { "x": 0, "y": 0, "z": 0 }; let pos = { "x": cmd.x, "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z), "z": cmd.z }; const yOrigin = cmd.yOrigin || 0; let range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, yOrigin, 2 * Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, data) { let templateName = data.templateName; let owner = data.player !== undefined ? data.player : player; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); if (!template) return null; let aurasTemplate = {}; if (!template.Auras) return GetTemplateDataHelper(template, owner, aurasTemplate, Resources); let auraNames = template.Auras._string.split(/\s+/); for (let name of auraNames) { let auraTemplate = AuraTemplates.Get(name); if (!auraTemplate) error("Template " + templateName + " has undefined aura " + name); else aurasTemplate[name] = auraTemplate; } return GetTemplateDataHelper(template, owner, aurasTemplate, Resources); }; GuiInterface.prototype.AreRequirementsMet = function(player, data) { return !data.requirements || RequirementsHelper.AreRequirementsMet(data.requirements, data.player !== undefined ? data.player : player); }; /** * Checks whether the requirements for this technology have been met. */ GuiInterface.prototype.CheckTechnologyRequirements = function(player, data) { let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(data.tech); }; /** * Returns technologies that are being actively researched, along with * which entity is researching them and how far along the research is. */ GuiInterface.prototype.GetStartedResearch = function(player) { return QueryPlayerIDInterface(player, IID_TechnologyManager)?.GetBasicInfoOfStartedTechs() || {}; }; /** * Returns the battle state of the player. */ GuiInterface.prototype.GetBattleState = function(player) { let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (!cmpBattleDetection) return false; return cmpBattleDetection.GetState(); }; /** * Returns a list of ongoing attacks against the player. */ GuiInterface.prototype.GetIncomingAttacks = function(player) { let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); if (!cmpAttackDetection) return []; return cmpAttackDetection.GetIncomingAttacks(); }; /** * Used to show a red square over GUI elements you can't yet afford. */ GuiInterface.prototype.GetNeededResources = function(player, data) { let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player); return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {}; }; /** * State of the templateData (player dependent): true when some template values have been modified * and need to be reloaded by the gui. */ GuiInterface.prototype.OnTemplateModification = function(msg) { this.templateModified[msg.player] = true; this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.IsTemplateModified = function(player) { return this.templateModified[player] || false; }; GuiInterface.prototype.ResetTemplateModified = function() { this.templateModified = {}; }; /** * Some changes may require an update to the selection panel, * which is cached for efficiency. Inform the GUI it needs reloading. */ GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.SetSelectionDirty = function(player) { this.selectionDirty[player] = true; }; GuiInterface.prototype.IsSelectionDirty = function(player) { return this.selectionDirty[player] || false; }; GuiInterface.prototype.ResetSelectionDirty = function() { this.selectionDirty = {}; }; /** * Add a timed notification. * Warning: timed notifacations are serialised * (to also display them on saved games or after a rejoin) * so they should allways be added and deleted in a deterministic way. */ GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); notification.endTime = duration + cmpTimer.GetTime(); notification.id = ++this.timeNotificationID; // Let all players and observers receive the notification by default. if (!notification.players) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } this.timeNotifications.push(notification); this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime); cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID); }; GuiInterface.prototype.GetTimeNotifications = function(player) { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); // Filter on players and time, since the delete timer might be executed with a delay. return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time); }; GuiInterface.prototype.PushNotification = function(notification) { if (!notification.type || notification.type == "text") this.AddTimeNotification(notification); else this.notifications.push(notification); }; GuiInterface.prototype.GetNotifications = function() { let n = this.notifications; this.notifications = []; return n; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { let cmpPlayer = QueryPlayerIDInterface(wantedPlayer); if (!cmpPlayer) return []; return cmpPlayer.GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return {}; return { "name": template.Identity.GenericName, "tooltip": template.Formation.DisabledTooltip || "", "icon": template.Identity.Icon }; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { return data.ents.some(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate; }); }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance) return true; } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { let buildableEnts = []; for (let ent of cmd.entities) { let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (let building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data) { let updateEntityColor = (iids, entities) => { for (let ent of entities) for (let iid of iids) { let cmp = Engine.QueryInterface(ent, iid); if (cmp) cmp.UpdateColor(); } }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) { - let cmpPlayer = QueryPlayerIDInterface(i, IID_Player); - if (!cmpPlayer) + const cmpDiplomacy = QueryPlayerIDInterface(i, IID_Diplomacy); + if (!cmpDiplomacy) continue; - cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors); + QueryPlayerIDInterface(i, IID_Player).SetDisplayDiplomacyColor(data.displayDiplomacyColors); if (data.displayDiplomacyColors) - cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]); + cmpDiplomacy.SetDiplomacyColor(data.displayedPlayerColors[i]); updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ? [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] : [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer], cmpRangeManager.GetEntitiesByPlayer(i)); } updateEntityColor([IID_Selectable, IID_StatusBars], data.selected); Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors(); }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { // Cache of owner -> color map let playerColors = {}; for (let ent of cmd.entities) { let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color. let owner = INVALID_PLAYER; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); let color = playerColors[owner]; if (!color) { - color = { "r": 1, "g": 1, "b": 1 }; - let cmpPlayer = QueryPlayerIDInterface(owner); - if (cmpPlayer) - color = cmpPlayer.GetDisplayedColor(); + color = QueryPlayerIDInterface(owner, IID_Player)?.GetDisplayedColor() || this.WHITE; playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER) continue; cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return Array.from(this.entsWithAuraAndStatusBars); }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { let affectedEnts = new Set(); for (let ent of cmd.entities) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (!cmpStatusBars) continue; cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) continue; for (let name of cmpAuras.GetAuraNames()) { if (!cmpAuras.GetOverlayIcon(name)) continue; for (let e of cmpAuras.GetAffectedEntities(name)) affectedEnts.add(e); if (cmd.enabled) this.entsWithAuraAndStatusBars.add(ent); else this.entsWithAuraAndStatusBars.delete(ent); } } for (let ent of affectedEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RegenerateSprites(); } }; GuiInterface.prototype.SetRangeOverlays = function(player, cmd) { for (let ent of cmd.entities) { let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player); }; GuiInterface.prototype.GetNonGaiaEntities = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); // If there are some rally points already displayed, first hide them. for (let ent of this.entsRallyPointsDisplayed) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities. for (let ent of cmd.entities) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // Entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location). let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner. let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position. let pos; if (cmd.x && cmd.z) pos = cmd; else // May return undefined if no rally point is set. pos = cmpRallyPoint.GetPositions()[0]; if (pos) { // Only update the position if we changed it (cmd.queued is set). // Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z. if ("queued" in cmd) { if (cmd.queued == true) cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z)); else cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z)); } else if (!cmpRallyPointRenderer.IsSet()) // Rebuild the renderer when not set (when reading saved game or in case of building update). for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z)); cmpRallyPointRenderer.SetDisplayed(true); // Remember which entities have their rally points displayed so we can hide them again. this.entsRallyPointsDisplayed.push(ent); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(cmd.owner); let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the message * "translateMessage": localisation info * "translateParameters": localisation info * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": localisation info (optional) * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { let result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [] }; if (!this.placementEntity || this.placementEntity[0] != cmd.template) { if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; let pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // Set it to a red shade if this is an invalid location. let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { let wallSet = cmd.wallSet; // Did the start position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let start = { "pos": cmd.start, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // Did the end position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let end = { "pos": cmd.end, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // -------------------------------------------------------------------------------- // Do some entity cache management and check for snapping. if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // We're clearing the preview, clear the entity cache and bail. for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // Keep template data around. } return false; } for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) { let pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before. for (let type in wallSet.templates) { if (type == "curves") continue; let tpl = wallSet.templates[type]; if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, { "templateName": tpl }), }; if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } // Prevent division by zero errors further on if the start and end positions are the same. if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { // Value of 0.5 was determined through trial and error. let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; let startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { let endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // Clear the single-building preview entity (we'll be rolling our own). this.SetBuildingPlacementPreview(player, { "template": "" }); // -------------------------------------------------------------------------------- // Calculate wall placement and position preview entities. let result = { "pieces": [], "cost": { "population": 0, "time": 0 } }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) // See helpers/Walls.js. previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group. let startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true // Preview only, must not appear in the result. }); } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle }); } if (end.pos) { // Analogous to the starting side case above. if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || []; previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup()); } // If we're snapping to a foundation, add an extra preview tower and also set it to the same control group. let endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true }); } } else previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle }); } let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. let allPiecesValid = true; // Number of entities that are required to build the entire wall, regardless of validity. let numRequiredPieces = 0; for (let i = 0; i < previewEntities.length; ++i) { let entInfo = previewEntities[i]; let ent = null; let tpl = entInfo.template; let tplData = this.placementWallEntities[tpl].templateData; let entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else ent = entPool.entities[entPool.numUsed]; if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // Move piece to right location. // TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities. let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces. if (tpl === wallSet.templates.tower) { let terrainGroundPrev = null; let terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { let targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. let primaryControlGroup = ent; let secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); let validPlacement = false; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region. // TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta. let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden"; if (visible) { let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement. validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success; // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) ++numRequiredPieces; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: We should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. for (let res of Resources.GetCodes().concat(["population", "time"])) result.cost[res] += tplData.cost[res]; } let canAfford = true; let cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) canAfford = false; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } ++entPool.numUsed; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest. // (TODO: Break unlikely ties by choosing the lowest entity ID.) let minDist2 = -1; let minDistEntitySnapData = null; let radius2 = data.snapRadius * data.snapRadius; for (let ent of data.snapEntities) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; let pos = cmpPosition.GetPosition(); let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = { "x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent }; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (data.snapToEdges) { let position = this.obstructionSnap.getPosition(data, template); if (position) return position; } if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return { "x": data.x, "z": data.z, "angle": angle }; } return false; }; GuiInterface.prototype.PlaySoundForPlayer = function(player, data) { let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player); let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(data.name); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player); }; GuiInterface.prototype.PlaySound = function(player, data) { if (!data.entity) return; PlaySound(data.name, data.entity); }; /** * Find any idle units. * * @param data.idleClasses Array of class names to include. * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). * @param data.excludeUnits Array of units to exclude. * * Returns an array of idle units. * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. */ GuiInterface.prototype.FindIdleUnits = function(player, data) { let idleUnits = []; // The general case is that only the 'first' idle unit is required; filtering would examine every unit. // This loop imitates a grouping/aggregation on the first matching idle class. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let entity of cmpRangeManager.GetEntitiesByPlayer(player)) { let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); if (!filtered.idle) continue; // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. // By adding to the 'end', there is no pause if the series of units loops. let bucket = filtered.bucket; if (bucket == 0 && data.prevUnit && entity <= data.prevUnit) bucket = data.idleClasses.length; if (!idleUnits[bucket]) idleUnits[bucket] = []; idleUnits[bucket].push(entity); // If enough units have been collected in the first bucket, go ahead and return them. if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) return idleUnits[0]; } let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); if (data.limit && reduced.length > data.limit) return reduced.slice(0, data.limit); return reduced; }; /** * Discover if the player has idle units. * * @param data.idleClasses Array of class names to include. * @param data.excludeUnits Array of units to exclude. * * Returns a boolean of whether the player has any idle units */ GuiInterface.prototype.HasIdleUnits = function(player, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); }; /** * Whether to filter an idle unit * * @param unit The unit to filter. * @param idleclasses Array of class names to include. * @param excludeUnits Array of units to exclude. * * Returns an object with the following fields: * - idle - true if the unit is considered idle by the filter, false otherwise. * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. */ GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle()) return { "idle": false }; let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable); if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned()) return { "idle": false }; const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable); if (cmpTurretable && cmpTurretable.IsTurreted()) return { "idle": false }; let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); if (!cmpIdentity) return { "idle": false }; let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); if (bucket == -1 || excludeUnits.indexOf(unit) > -1) return { "idle": false }; return { "idle": true, "bucket": bucket }; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market); return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; let firstMarket = cmpEntityTrader.GetFirstMarket(); let secondMarket = cmpEntityTrader.GetSecondMarket(); let result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGoods().amount; } else if (data.target === secondMarket) result = { "type": "is second", "gain": cmpEntityTrader.GetGoods().amount, }; else if (!firstMarket) result = { "type": "set first" }; else if (!secondMarket) result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; else result = { "type": "set first" }; return result; }; GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { return Engine.QueryInterface(data.entity, IID_Trainer)?.GetBatchTime(data.batchSize) || 0; }; GuiInterface.prototype.IsMapRevealed = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for (let ent of data.entities) { let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader)); let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; let shipTrader = { "total": 0, "trading": 0 }; for (let ent of traders) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { let holder = cmpUnitAI.order.data.target; let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return []; return cmpPlayer.GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); }; /** * List the GuiInterface functions that can be safely called by GUI scripts. * (GUI scripts are non-deterministic and untrusted, so these functions must be * appropriately careful. They are called with a first argument "player", which is * trusted and indicates the player associated with the current client; no data should * be returned unless this player is meant to be able to see it.) */ let exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetInitAttributes": 1, "GetReplayMetadata": 1, "GetCampaignGameEndData": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetMultipleEntityStates": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "AreRequirementsMet": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNotifications": 1, "GetTimeNotifications": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, "UpdateDisplayedPlayerColors": 1, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, "DisplayRallyPoint": 1, "AddTargetMarker": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "PlaySoundForPlayer": 1, "FindIdleUnits": 1, "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetPathfinderHierDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "EnableVisualRangeOverlayType": 1, "SetRangeOverlays": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, "IsTemplateModified": 1, "ResetTemplateModified": 1, "IsSelectionDirty": 1, "ResetSelectionDirty": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); throw new Error("Invalid GuiInterface Call name \"" + name + "\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 27722) @@ -1,991 +1,765 @@ function Player() {} Player.prototype.Schema = "" + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + - "" + - "" + - "" + - "" + - "" + - "" + "" + "" + ""; // The GUI expects these strings. Player.prototype.STATE_ACTIVE = "active"; Player.prototype.STATE_DEFEATED = "defeated"; Player.prototype.STATE_WON = "won"; -/** - * Don't serialize diplomacyColor or displayDiplomacyColor since they're modified by the GUI. - */ Player.prototype.Serialize = function() { let state = {}; for (let key in this) if (this.hasOwnProperty(key)) state[key] = this[key]; - state.diplomacyColor = undefined; - state.displayDiplomacyColor = false; + // Modified by GUI, so don't serialise. + delete state.displayDiplomacyColor; return state; }; Player.prototype.Deserialize = function(state) { for (let prop in state) this[prop] = state[prop]; }; /** * Which units will be shown with special icons at the top. */ var panelEntityClasses = "Hero Relic"; Player.prototype.Init = function() { this.playerID = undefined; this.color = undefined; - this.diplomacyColor = undefined; - this.displayDiplomacyColor = false; 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 = {}; this.resourceGatherers = {}; this.tradingGoods = []; // Goods for next trade-route and its probabilities * 100. - 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 = this.STATE_ACTIVE; - this.diplomacy = []; // Array of diplomatic stances for this player with respect to other players (including gaia and self). - this.sharedDropsites = false; this.formations = this.template.Formations._string.split(" "); this.startCam = undefined; this.controlAllUnits = false; this.isAI = false; this.cheatsEnabled = false; this.panelEntities = []; this.resourceNames = {}; this.disabledTemplates = {}; this.disabledTechnologies = {}; this.spyCostMultiplier = +this.template.SpyCostMultiplier; this.barterEntities = []; this.barterMultiplier = { "buy": clone(this.template.BarterMultiplier.Buy), "sell": clone(this.template.BarterMultiplier.Sell) }; // Initial resources. let resCodes = Resources.GetCodes(); for (let res of resCodes) { this.resourceCount[res] = 300; this.resourceNames[res] = Resources.GetResource(res).name; this.resourceGatherers[res] = 0; } // Trading goods probability in steps of 5. let resTradeCodes = Resources.GetTradableCodes(); let quotient = Math.floor(20 / resTradeCodes.length); let remainder = 20 % resTradeCodes.length; for (let i in resTradeCodes) this.tradingGoods.push({ "goods": resTradeCodes[i], "proba": 5 * (quotient + (+i < remainder ? 1 : 0)) }); }; Player.prototype.SetPlayerID = function(id) { this.playerID = id; }; Player.prototype.GetPlayerID = function() { return this.playerID; }; Player.prototype.SetColor = function(r, g, b) { let colorInitialized = !!this.color; this.color = { "r": r / 255, "g": g / 255, "b": b / 255, "a": 1 }; // Used in Atlas. if (colorInitialized) Engine.BroadcastMessage(MT_PlayerColorChanged, { "player": this.playerID }); }; -Player.prototype.SetDiplomacyColor = function(color) -{ - this.diplomacyColor = { "r": color.r / 255, "g": color.g / 255, "b": color.b / 255, "a": 1 }; -}; - Player.prototype.SetDisplayDiplomacyColor = function(displayDiplomacyColor) { this.displayDiplomacyColor = displayDiplomacyColor; }; Player.prototype.GetColor = function() { return this.color; }; Player.prototype.GetDisplayedColor = function() { - return this.displayDiplomacyColor ? this.diplomacyColor : this.color; + return this.displayDiplomacyColor ? Engine.QueryInterface(this.entity, IID_Diplomacy).GetColor() : this.color; }; // 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.popUsed)) return num - (this.GetPopulationLimit() - this.popUsed); this.popUsed += num; return 0; }; Player.prototype.UnReservePopulationSlots = function(num) { this.popUsed -= num; }; Player.prototype.GetPopulationCount = function() { return this.popUsed; }; Player.prototype.AddPopulation = function(num) { this.popUsed += num; }; 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(ApplyValueModificationsToEntity("Player/MaxPopulation", this.maxPop, this.entity)); }; Player.prototype.CanBarter = function() { return this.barterEntities.length > 0; }; Player.prototype.GetBarterMultiplier = function() { return this.barterMultiplier; }; Player.prototype.GetSpyCostMultiplier = function() { return this.spyCostMultiplier; }; Player.prototype.GetPanelEntities = function() { return this.panelEntities; }; 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) { for (let res in resources) this.resourceCount[res] = resources[res]; }; Player.prototype.GetResourceCounts = function() { return this.resourceCount; }; Player.prototype.GetResourceGatherers = function() { return this.resourceGatherers; }; /** * @param {string} type - The generic type of resource to add the gatherer for. */ Player.prototype.AddResourceGatherer = function(type) { ++this.resourceGatherers[type]; }; /** * @param {string} type - The generic type of resource to remove the gatherer from. */ Player.prototype.RemoveResourceGatherer = function(type) { --this.resourceGatherers[type]; }; /** * Add resource of specified type to player. * @param {string} type - Generic type of resource. * @param {number} amount - Amount of resource, which should be added. */ Player.prototype.AddResource = function(type, amount) { this.resourceCount[type] += +amount; }; /** * Add resources to player. */ Player.prototype.AddResources = function(amounts) { for (let type in amounts) this.resourceCount[type] += +amounts[type]; }; Player.prototype.GetNeededResources = function(amounts) { // Check if we can afford it all. let amountsNeeded = {}; for (let 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) { let amountsNeeded = this.GetNeededResources(amounts); // If we don't have enough resources, send a notification to the player. if (amountsNeeded) { let parameters = {}; let i = 0; for (let type in amountsNeeded) { ++i; parameters["resourceType" + i] = this.resourceNames[type]; parameters["resourceAmount" + i] = amountsNeeded[type]; } let 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"); // Send as time-notification. let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [this.playerID], "message": msg, "parameters": parameters, "translateMessage": true, "translateParameters": { "resourceType1": "withinSentence", "resourceType2": "withinSentence", "resourceType3": "withinSentence", "resourceType4": "withinSentence" } }); return false; } for (let type in amounts) this.resourceCount[type] -= amounts[type]; return true; }; Player.prototype.TrySubtractResources = function(amounts) { if (!this.SubtractResourcesOrNotify(amounts)) return false; let cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (let type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]); return true; }; Player.prototype.RefundResources = function(amounts) { const cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (const type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, -amounts[type]); this.AddResources(amounts); }; Player.prototype.GetNextTradingGoods = function() { let value = randFloat(0, 100); let last = this.tradingGoods.length - 1; let sumProba = 0; for (let 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() { let tradingGoods = {}; for (let resource of this.tradingGoods) tradingGoods[resource.goods] = resource.proba; return tradingGoods; }; Player.prototype.SetTradingGoods = function(tradingGoods) { let resTradeCodes = Resources.GetTradableCodes(); let sumProba = 0; for (let resource in tradingGoods) { if (resTradeCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0) { error("Invalid trading goods: " + uneval(tradingGoods)); return; } sumProba += tradingGoods[resource]; } if (sumProba != 100) { error("Invalid trading goods probability: " + uneval(sumProba)); return; } this.tradingGoods = []; for (let resource in tradingGoods) this.tradingGoods.push({ "goods": resource, "proba": tradingGoods[resource] }); }; /** * @param {string} message - The message to send in the chat. May be undefined. */ Player.prototype.Win = function(message) { this.SetState(this.STATE_WON, message); }; /** * @param {string} message - The message to send in the chat. May be undefined. */ Player.prototype.Defeat = function(message) { this.SetState(this.STATE_DEFEATED, message); }; /** * @return {string} - The string identified with the current state. */ Player.prototype.GetState = function() { return this.state; }; /** * @return {boolean} - */ Player.prototype.IsActive = function() { return this.state === this.STATE_ACTIVE; }; /** * @return {boolean} - */ Player.prototype.IsDefeated = function() { return this.state === this.STATE_DEFEATED; }; /** * @return {boolean} - */ Player.prototype.HasWon = function() { return this.state === this.STATE_WON; }; /** * @param {string} newState - Either "defeated" or "won". * @param {string|undefined} message - A string to be shown in chat, for example * markForTranslation("%(player)s has been defeated (failed objective)."). * If it is undefined, the caller MUST send that GUI notification manually. */ Player.prototype.SetState = function(newState, message) { if (!this.IsActive()) return; if (newState != this.STATE_WON && newState != this.STATE_DEFEATED) { warn("Can't change playerstate to " + newState); return; } if (!this.playerID) { warn("Gaia can't change state."); return; } this.state = newState; const won = this.HasWon(); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (won) cmpRangeManager.SetLosRevealAll(this.playerID, true); else { // Reassign all player's entities to Gaia. let 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 (let entity of entities) { let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); cmpOwnership.SetOwnerQuiet(0); } // With the real ownership change complete, send OwnershipChanged messages. for (let entity of entities) Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity, "from": this.playerID, "to": 0 }); } if (message) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": won ? "won" : "defeat", "players": [this.playerID], "allies": [this.playerID], "message": message }); Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID }); }; -Player.prototype.GetTeam = function() -{ - return this.team; -}; - -Player.prototype.SetTeam = function(team) -{ - if (this.teamsLocked) - return; - - this.team = team; - - // Set all team members as allies. - if (this.team != -1) - { - let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); - for (let i = 0; i < numPlayers; ++i) - { - let cmpPlayer = QueryPlayerIDInterface(i); - if (this.team != cmpPlayer.GetTeam()) - continue; - - this.SetAlly(i); - cmpPlayer.SetAlly(this.playerID); - } - } - - Engine.BroadcastMessage(MT_DiplomacyChanged, { - "player": this.playerID, - "otherPlayer": null - }); -}; - -Player.prototype.SetLockTeams = function(value) -{ - this.teamsLocked = value; -}; - -Player.prototype.GetLockTeams = function() -{ - return this.teamsLocked; -}; - -Player.prototype.GetDiplomacy = function() -{ - return this.diplomacy.slice(); -}; - -Player.prototype.SetDiplomacy = function(dipl) -{ - this.diplomacy = dipl.slice(); - - Engine.BroadcastMessage(MT_DiplomacyChanged, { - "player": this.playerID, - "otherPlayer": null - }); -}; - -Player.prototype.SetDiplomacyIndex = function(idx, value) -{ - let cmpPlayer = QueryPlayerIDInterface(idx); - if (!cmpPlayer) - return; - - if (!this.IsActive() || !cmpPlayer.IsActive()) - return; - - this.diplomacy[idx] = value; - - Engine.BroadcastMessage(MT_DiplomacyChanged, { - "player": this.playerID, - "otherPlayer": cmpPlayer.GetPlayerID() - }); - - // Mutual worsening of relations. - if (cmpPlayer.diplomacy[this.playerID] > value) - cmpPlayer.SetDiplomacyIndex(this.playerID, value); -}; - -Player.prototype.UpdateSharedLos = function() -{ - let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); - if (!cmpRangeManager || !cmpTechnologyManager) - return; - - if (!cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech)) - { - cmpRangeManager.SetSharedLos(this.playerID, [this.playerID]); - return; - } - - cmpRangeManager.SetSharedLos(this.playerID, this.GetMutualAllies()); -}; - 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.HasSharedLos = function() -{ - let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); - return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech); -}; -Player.prototype.HasSharedDropsites = function() -{ - return this.sharedDropsites; -}; - 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.GetPlayersByDiplomacy = function(func) -{ - let players = []; - for (let i = 0; i < this.diplomacy.length; ++i) - if (this[func](i)) - players.push(i); - return players; -}; - -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; -}; - -Player.prototype.GetAllies = function() -{ - return this.GetPlayersByDiplomacy("IsAlly"); -}; - -/** - * Check if given player is our ally excluding ourself - */ -Player.prototype.IsExclusiveAlly = function(id) -{ - return this.playerID != id && this.IsAlly(id); -}; - -/** - * Check if given player is our ally, and we are its ally - */ -Player.prototype.IsMutualAlly = function(id) -{ - let cmpPlayer = QueryPlayerIDInterface(id); - return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID); -}; - -Player.prototype.GetMutualAllies = function() -{ - return this.GetPlayersByDiplomacy("IsMutualAlly"); -}; - -/** - * Check if given player is our ally, and we are its ally, excluding ourself - */ -Player.prototype.IsExclusiveMutualAlly = function(id) -{ - return this.playerID != id && this.IsMutualAlly(id); -}; - -Player.prototype.SetEnemy = function(id) -{ - this.SetDiplomacyIndex(id, -1); -}; - -/** - * Check if given player is our enemy - */ -Player.prototype.IsEnemy = function(id) -{ - return this.diplomacy[id] < 0; -}; - -Player.prototype.GetEnemies = function() -{ - return this.GetPlayersByDiplomacy("IsEnemy"); -}; - -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; -}; - /** * Do some map dependant initializations */ Player.prototype.OnGlobalInitGame = function(msg) { // Replace the "{civ}" code with this civ ID. let disabledTemplates = this.disabledTemplates; this.disabledTemplates = {}; const civ = Engine.QueryInterface(this.entity, IID_Identity).GetCiv(); for (let template in disabledTemplates) if (disabledTemplates[template]) this.disabledTemplates[template.replace(/\{civ\}/g, civ)] = true; }; /** * 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; let cmpCost = Engine.QueryInterface(msg.entity, IID_Cost); if (msg.from == this.playerID) { if (cmpCost) this.popUsed -= cmpCost.GetPopCost(); let panelIndex = this.panelEntities.indexOf(msg.entity); if (panelIndex >= 0) this.panelEntities.splice(panelIndex, 1); let barterIndex = this.barterEntities.indexOf(msg.entity); if (barterIndex >= 0) this.barterEntities.splice(barterIndex, 1); } if (msg.to == this.playerID) { if (cmpCost) this.popUsed += cmpCost.GetPopCost(); let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; if (MatchesClassList(cmpIdentity.GetClassesList(), panelEntityClasses)) this.panelEntities.push(msg.entity); if (cmpIdentity.HasClass("Barter") && !Engine.QueryInterface(msg.entity, IID_Foundation)) this.barterEntities.push(msg.entity); } }; -Player.prototype.OnResearchFinished = function(msg) -{ - if (msg.tech == this.template.SharedLosTech) - this.UpdateSharedLos(); - else if (msg.tech == this.template.SharedDropsitesTech) - this.sharedDropsites = true; -}; - -Player.prototype.OnDiplomacyChanged = function() -{ - this.UpdateSharedLos(); -}; - Player.prototype.OnValueModification = function(msg) { if (msg.component != "Player") return; if (msg.valueNames.indexOf("Player/SpyCostMultiplier") != -1) this.spyCostMultiplier = ApplyValueModificationsToEntity("Player/SpyCostMultiplier", +this.template.SpyCostMultiplier, this.entity); if (msg.valueNames.some(mod => mod.startsWith("Player/BarterMultiplier/"))) for (let res in this.template.BarterMultiplier.Buy) { this.barterMultiplier.buy[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Buy/"+res, +this.template.BarterMultiplier.Buy[res], this.entity); this.barterMultiplier.sell[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Sell/"+res, +this.template.BarterMultiplier.Sell[res], this.entity); } }; Player.prototype.SetCheatsEnabled = function(flag) { this.cheatsEnabled = flag; }; Player.prototype.GetCheatsEnabled = function() { return this.cheatsEnabled; }; Player.prototype.TributeResource = function(player, amounts) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; if (!this.IsActive() || !cmpPlayer.IsActive()) return; let resTribCodes = Resources.GetTributableCodes(); for (let resCode in amounts) if (resTribCodes.indexOf(resCode) == -1 || !Number.isInteger(amounts[resCode]) || amounts[resCode] < 0) { warn("Invalid tribute amounts: " + uneval(resCode) + ": " + uneval(amounts)); return; } if (!this.SubtractResourcesOrNotify(amounts)) return; cmpPlayer.AddResources(amounts); let total = Object.keys(amounts).reduce((sum, type) => sum + amounts[type], 0); let cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpOurStatisticsTracker) cmpOurStatisticsTracker.IncreaseTributesSentCounter(total); let cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpTheirStatisticsTracker) cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification({ "type": "tribute", "players": [player], "donator": this.playerID, "amounts": amounts }); Engine.BroadcastMessage(MT_TributeExchanged, { "to": player, "from": this.playerID, "amounts": amounts }); }; Player.prototype.AddDisabledTemplate = function(template) { this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.RemoveDisabledTemplate = function(template) { this.disabledTemplates[template] = false; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.SetDisabledTemplates = function(templates) { this.disabledTemplates = {}; for (let template of templates) this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.GetDisabledTemplates = function() { return this.disabledTemplates; }; Player.prototype.AddDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.RemoveDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = false; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.SetDisabledTechnologies = function(techs) { this.disabledTechnologies = {}; for (let tech of techs) this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.GetDisabledTechnologies = function() { return this.disabledTechnologies; }; Player.prototype.OnGlobalPlayerDefeated = function(msg) { let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (!cmpSound) return; - const soundGroup = cmpSound.GetSoundGroup(this.playerID === msg.playerId ? "defeated" : this.IsAlly(msg.playerId) ? "defeated_ally" : this.HasWon() ? "won" : "defeated_enemy"); + const soundGroup = cmpSound.GetSoundGroup(this.playerID === msg.playerId ? "defeated" : Engine.QueryInterface(this.entity, IID_Diplomacy).IsAlly(msg.playerId) ? "defeated_ally" : this.HasWon() ? "won" : "defeated_enemy"); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, this.playerID); }; Engine.RegisterComponentType(IID_Player, "Player", Player); Index: ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js (revision 27722) @@ -1,187 +1,186 @@ function PlayerManager() {} PlayerManager.prototype.Schema = ""; PlayerManager.prototype.Init = function() { // List of player entity IDs. this.playerEntities = []; }; /** * @param {string} templateName - The template name of the player to add. * @return {number} - The player's ID (player number). */ PlayerManager.prototype.AddPlayer = function(templateName) { const ent = Engine.AddEntity(templateName); const id = this.playerEntities.length; - const cmpPlayer = Engine.QueryInterface(ent, IID_Player); - cmpPlayer.SetPlayerID(id); + Engine.QueryInterface(ent, IID_Player).SetPlayerID(id); this.playerEntities.push(ent); const newDiplo = []; for (let i = 0; i < id; i++) { - Engine.QueryInterface(this.GetPlayerByID(i), IID_Player).diplomacy[id] = -1; + Engine.QueryInterface(this.GetPlayerByID(i), IID_Diplomacy).diplomacy[id] = -1; newDiplo[i] = -1; } newDiplo[id] = 1; - cmpPlayer.SetDiplomacy(newDiplo); + Engine.QueryInterface(ent, IID_Diplomacy).SetDiplomacy(newDiplo); Engine.BroadcastMessage(MT_PlayerEntityChanged, { "player": id, "from": INVALID_ENTITY, "to": ent }); return id; }; /** * To avoid possible problems, * we first remove all entities from this player, and add them back after the replacement. * Note: This should only be called during setup/init and not during the game * @param {number} id - The player number to replace. * @param {string} newTemplateName - The new template name for the player. */ PlayerManager.prototype.ReplacePlayerTemplate = function(id, newTemplateName) { const ent = Engine.AddEntity(newTemplateName); const entities = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(id); for (const e of entities) Engine.QueryInterface(e, IID_Ownership)?.SetOwner(INVALID_PLAYER); const oldent = this.playerEntities[id]; const oldCmpPlayer = Engine.QueryInterface(oldent, IID_Player); const newCmpPlayer = Engine.QueryInterface(ent, IID_Player); newCmpPlayer.SetPlayerID(id); this.playerEntities[id] = ent; newCmpPlayer.SetColor(oldCmpPlayer.GetColor()); - newCmpPlayer.SetDiplomacy(oldCmpPlayer.GetDiplomacy()); + Engine.QueryInterface(ent, IID_Diplomacy).SetDiplomacy(Engine.QueryInterface(oldent, IID_Diplomacy).GetDiplomacy()); Engine.BroadcastMessage(MT_PlayerEntityChanged, { "player": id, "from": oldent, "to": ent }); for (const e of entities) Engine.QueryInterface(e, IID_Ownership)?.SetOwner(id); Engine.DestroyEntity(oldent); Engine.FlushDestroyedEntities(); }; /** * Returns the player entity ID for the given player ID. * The player ID must be valid (else there will be an error message). */ PlayerManager.prototype.GetPlayerByID = function(id) { if (id in this.playerEntities) return this.playerEntities[id]; // Observers don't have player data. if (id == INVALID_PLAYER) return INVALID_ENTITY; const stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line warn("GetPlayerByID: no player defined for id '"+id+"'\n"+stack); return INVALID_ENTITY; }; /** * Returns the number of players including gaia. */ PlayerManager.prototype.GetNumPlayers = function() { return this.playerEntities.length; }; /** * Returns IDs of all players including gaia. */ PlayerManager.prototype.GetAllPlayers = function() { const players = []; for (let i = 0; i < this.playerEntities.length; ++i) players.push(i); return players; }; /** * Returns IDs of all players excluding gaia. */ PlayerManager.prototype.GetNonGaiaPlayers = function() { const players = []; for (let i = 1; i < this.playerEntities.length; ++i) players.push(i); return players; }; /** * Returns IDs of all players excluding gaia that are not defeated nor have won. */ PlayerManager.prototype.GetActivePlayers = function() { return this.GetNonGaiaPlayers().filter(playerID => Engine.QueryInterface(this.GetPlayerByID(playerID), IID_Player).IsActive() ); }; /** * Note: This should only be called during setup/init and not during a match * since it doesn't change the owned entities. */ PlayerManager.prototype.RemoveLastPlayer = function() { if (!this.playerEntities.length) return; const lastId = this.playerEntities.pop(); Engine.BroadcastMessage(MT_PlayerEntityChanged, { "player": this.playerEntities.length + 1, "from": lastId, "to": INVALID_ENTITY }); Engine.DestroyEntity(lastId); }; PlayerManager.prototype.SetMaxWorldPopulation = function(max) { this.maxWorldPopulation = max; this.RedistributeWorldPopulation(); }; PlayerManager.prototype.GetMaxWorldPopulation = function() { return this.maxWorldPopulation; }; PlayerManager.prototype.RedistributeWorldPopulation = function() { const worldPopulation = this.GetMaxWorldPopulation(); if (!worldPopulation) return; const activePlayers = this.GetActivePlayers(); if (!activePlayers.length) return; const newMaxPopulation = worldPopulation / activePlayers.length; for (const playerID of activePlayers) Engine.QueryInterface(this.GetPlayerByID(playerID), IID_Player).SetMaxPopulation(newMaxPopulation); }; PlayerManager.prototype.OnGlobalPlayerDefeated = function(msg) { this.RedistributeWorldPopulation(); }; Engine.RegisterSystemComponentType(IID_PlayerManager, "PlayerManager", PlayerManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 27722) @@ -1,521 +1,526 @@ function ResourceGatherer() {} ResourceGatherer.prototype.Schema = "Lets the unit gather resources from entities that have the ResourceSupply component." + "" + "2.0" + "1.0" + "" + "1" + "3" + "3" + "2" + "" + "" + "10" + "10" + "10" + "10" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("positiveDecimal", [], true) + "" + "" + Resources.BuildSchema("positiveDecimal") + ""; /* * Call interval will be determined by gather rate, * so always gather integer amount. */ ResourceGatherer.prototype.GATHER_AMOUNT = 1; ResourceGatherer.prototype.Init = function() { this.capacities = {}; this.carrying = {}; // { generic type: integer amount currently carried } // (Note that this component supports carrying multiple types of resources, // each with an independent capacity, but the rest of the game currently // ensures and assumes we'll only be carrying one type at once) // The last exact type gathered, so we can render appropriate props this.lastCarriedType = undefined; // { generic, specific } }; /** * Returns data about what resources the unit is currently carrying, * in the form [ {"type":"wood", "amount":7, "max":10} ] */ ResourceGatherer.prototype.GetCarryingStatus = function() { let ret = []; for (let type in this.carrying) { ret.push({ "type": type, "amount": this.carrying[type], "max": +this.GetCapacity(type) }); } return ret; }; /** * Used to instantly give resources to unit * @param resources The same structure as returned form GetCarryingStatus */ ResourceGatherer.prototype.GiveResources = function(resources) { for (let resource of resources) this.carrying[resource.type] = +resource.amount; }; /** * Returns the generic type of one particular resource this unit is * currently carrying, or undefined if none. */ ResourceGatherer.prototype.GetMainCarryingType = function() { // Return the first key, if any for (let type in this.carrying) return type; return undefined; }; /** * Returns the exact resource type we last picked up, as long as * we're still carrying something similar enough, in the form * { generic, specific } */ ResourceGatherer.prototype.GetLastCarriedType = function() { if (this.lastCarriedType && this.lastCarriedType.generic in this.carrying) return this.lastCarriedType; return undefined; }; ResourceGatherer.prototype.SetLastCarriedType = function(lastCarriedType) { this.lastCarriedType = lastCarriedType; }; // Since this code is very performancecritical and applying technologies quite slow, cache it. ResourceGatherer.prototype.RecalculateGatherRates = function() { this.baseSpeed = ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity); this.rates = {}; for (let r in this.template.Rates) { let type = r.split("."); if (!Resources.GetResource(type[0]).subtypes[type[1]]) { error("Resource subtype not found: " + type[0] + "." + type[1]); continue; } let rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity); this.rates[r] = rate * this.baseSpeed; } }; ResourceGatherer.prototype.RecalculateCapacities = function() { this.capacities = {}; for (let r in this.template.Capacities) this.capacities[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], this.entity); }; ResourceGatherer.prototype.RecalculateCapacity = function(type) { if (type in this.capacities) this.capacities[type] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + type, +this.template.Capacities[type], this.entity); }; ResourceGatherer.prototype.GetGatherRates = function() { return this.rates; }; ResourceGatherer.prototype.GetGatherRate = function(resourceType) { if (!this.template.Rates[resourceType]) return 0; return this.rates[resourceType]; }; ResourceGatherer.prototype.GetCapacity = function(resourceType) { if (!this.template.Capacities[resourceType]) return 0; return this.capacities[resourceType]; }; ResourceGatherer.prototype.GetRange = function() { return { "max": +this.template.MaxDistance, "min": 0 }; }; /** * @param {number} target - The target to gather from. * @param {number} callerIID - The IID to notify on specific events. * @return {boolean} - Whether we started gathering. */ ResourceGatherer.prototype.StartGathering = function(target, callerIID) { if (this.target) this.StopGathering(); let rate = this.GetTargetGatherRate(target); if (!rate) return false; let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); if (!cmpResourceSupply || !cmpResourceSupply.AddActiveGatherer(this.entity)) return false; let resourceType = cmpResourceSupply.GetType(); // If we've already got some resources but they're the wrong type, // drop them first to ensure we're only ever carrying one type. if (this.IsCarryingAnythingExcept(resourceType.generic)) this.DropResources(); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("gather_" + resourceType.specific, false, 1.0); // Calculate timing based on gather rates. // This allows the gather rate to control how often we gather, instead of how much. let timing = 1000 / rate; this.target = target; this.callerIID = callerIID; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_ResourceGatherer, "PerformGather", timing, timing, null); return true; }; /** * @param {string} reason - The reason why we stopped gathering used to notify the caller. */ ResourceGatherer.prototype.StopGathering = function(reason) { if (!this.target) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); delete this.timer; let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply); if (cmpResourceSupply) cmpResourceSupply.RemoveGatherer(this.entity); delete this.target; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; if (reason && callerIID) { let component = Engine.QueryInterface(this.entity, callerIID); if (component) component.ProcessMessage(reason, null); } }; /** * Gather from our target entity. * @params - data and lateness are unused. */ ResourceGatherer.prototype.PerformGather = function(data, lateness) { let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply); if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0) { this.StopGathering("TargetInvalidated"); return; } if (!this.IsTargetInRange(this.target)) { this.StopGathering("OutOfRange"); return; } // ToDo: Enable entities to keep facing a target. Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); let type = cmpResourceSupply.GetType(); if (!this.carrying[type.generic]) this.carrying[type.generic] = 0; let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic]; let status = cmpResourceSupply.TakeResources(Math.min(this.GATHER_AMOUNT, maxGathered)); this.carrying[type.generic] += status.amount; this.lastCarriedType = type; // Update stats of how much the player collected. // (We have to do it here rather than at the dropsite, because we // need to know what subtype it was.) let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific); if (!this.CanCarryMore(type.generic)) this.StopGathering("InventoryFilled"); else if (status.exhausted) this.StopGathering("TargetInvalidated"); }; /** * Compute the amount of resources collected per second from the target. * Returns 0 if resources cannot be collected (e.g. the target doesn't * exist, or is the wrong type). */ ResourceGatherer.prototype.GetTargetGatherRate = function(target) { let cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply); if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0) return 0; let type = cmpResourceSupply.GetType(); let rate = 0; if (type.specific) rate = this.GetGatherRate(type.generic + "." + type.specific); if (rate == 0 && type.generic) rate = this.GetGatherRate(type.generic); let diminishingReturns = cmpResourceSupply.GetDiminishingReturns(); if (diminishingReturns) rate *= diminishingReturns; return rate; }; /** * @param {number} target - The entity ID of the target to check. * @return {boolean} - Whether we can gather from the target. */ ResourceGatherer.prototype.CanGather = function(target) { return this.GetTargetGatherRate(target) > 0; }; /** * Returns whether this unit can carry more of the given type of resource. * (This ignores whether the unit is actually able to gather that * resource type or not.) */ ResourceGatherer.prototype.CanCarryMore = function(type) { let amount = this.carrying[type] || 0; return amount < this.GetCapacity(type); }; ResourceGatherer.prototype.IsCarrying = function(type) { let amount = this.carrying[type] || 0; return amount > 0; }; /** * Returns whether this unit is carrying any resources of a type that is * not the requested type. (This is to support cases where the unit is * only meant to be able to carry one type at once.) */ ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType) { for (let type in this.carrying) if (type != exceptedType) return true; return false; }; /** * @param {number} target - The entity to check. * @param {boolean} checkCarriedResource - Whether we need to check the resource we are carrying. * @return {boolean} - Whether we can return carried resources. */ ResourceGatherer.prototype.CanReturnResource = function(target, checkCarriedResource) { let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return false; if (checkCarriedResource) { let type = this.GetMainCarryingType(); if (!type || !cmpResourceDropsite.AcceptsType(type)) return false; } - let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target)) + const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership) + return false; + + const playerID = cmpOwnership.GetOwner(); + if (IsOwnedByPlayer(playerID, target)) return true; - let cmpPlayer = QueryOwnerInterface(this.entity); - return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() && - cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target); + + return QueryPlayerIDInterface(playerID, IID_Diplomacy)?.HasSharedDropsites() && + cmpResourceDropsite.IsShared() && + IsOwnedByMutualAllyOfPlayer(playerID, target); }; /** * Transfer our carried resources to our owner immediately. * Only resources of the appropriate types will be transferred. * (This should typically be called after reaching a dropsite.) * * @param {number} target - The target entity ID to drop resources at. */ ResourceGatherer.prototype.CommitResources = function(target) { let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return; let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity); for (let type in change) { this.carrying[type] -= change[type]; if (this.carrying[type] == 0) delete this.carrying[type]; } }; /** * Drop all currently-carried resources. * (Currently they just vanish after being dropped - we don't bother depositing * them onto the ground.) */ ResourceGatherer.prototype.DropResources = function() { this.carrying = {}; }; /** * @return {string} - A generic resource type if we were tasked to gather. */ ResourceGatherer.prototype.GetTaskedResourceType = function() { return this.taskedResourceType; }; /** * @param {string} type - A generic resource type. */ ResourceGatherer.prototype.AddToPlayerCounter = function(type) { // We need to be removed from the player counter first. if (this.taskedResourceType) return; let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) cmpPlayer.AddResourceGatherer(type); this.taskedResourceType = type; }; /** * @param {number} playerid - Optionally a player ID. */ ResourceGatherer.prototype.RemoveFromPlayerCounter = function(playerid) { if (!this.taskedResourceType) return; let cmpPlayer = playerid != undefined ? QueryPlayerIDInterface(playerid) : QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) cmpPlayer.RemoveResourceGatherer(this.taskedResourceType); delete this.taskedResourceType; }; /** * @param {number} - The entity ID of the target to check. * @return {boolean} - Whether this entity is in range of its target. */ ResourceGatherer.prototype.IsTargetInRange = function(target) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager). IsInTargetRange(this.entity, target, 0, +this.template.MaxDistance, false); }; // Since we cache gather rates, we need to make sure we update them when tech changes. // and when our owner change because owners can had different techs. ResourceGatherer.prototype.OnValueModification = function(msg) { if (msg.component != "ResourceGatherer") return; // NB: at the moment, 0 A.D. always uses the fast path, the other is mod support. if (msg.valueNames.length === 1) { if (msg.valueNames[0].indexOf("Capacities") !== -1) this.RecalculateCapacity(msg.valueNames[0].substr(28)); else this.RecalculateGatherRates(); } else { this.RecalculateGatherRates(); this.RecalculateCapacities(); } }; ResourceGatherer.prototype.OnOwnershipChanged = function(msg) { if (msg.to == INVALID_PLAYER) { this.RemoveFromPlayerCounter(msg.from); return; } if (this.lastGathered && msg.from !== INVALID_PLAYER) { const resource = this.taskedResourceType; this.RemoveFromPlayerCounter(msg.from); this.AddToPlayerCounter(resource); } this.RecalculateGatherRates(); this.RecalculateCapacities(); }; ResourceGatherer.prototype.OnGlobalInitGame = function(msg) { this.RecalculateGatherRates(); this.RecalculateCapacities(); }; ResourceGatherer.prototype.OnMultiplierChanged = function(msg) { let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer && msg.player == cmpPlayer.GetPlayerID()) this.RecalculateGatherRates(); }; Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer); Index: ps/trunk/binaries/data/mods/public/simulation/components/StatisticsTracker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/StatisticsTracker.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/StatisticsTracker.js (revision 27722) @@ -1,522 +1,526 @@ function StatisticsTracker() {} StatisticsTracker.prototype.Schema = "This component records statistics over the course of the match, such as the number of trained, lost, captured and destroyed units and buildings The statistics are consumed by the summary screen and lobby rankings." + "" + "Infantry FemaleCitizen" + "House Wonder" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "tokens" + "" + "" + ""; /** * This number specifies the time in milliseconds between consecutive statistics snapshots recorded. */ StatisticsTracker.prototype.UpdateSequenceInterval = 30 * 1000; StatisticsTracker.prototype.Init = function() { this.unitsClasses = this.template.UnitClasses._string.split(/\s+/); this.buildingsClasses = this.template.StructureClasses._string.split(/\s+/); this.unitsTrained = {}; this.unitsLost = {}; this.enemyUnitsKilled = {}; this.unitsCaptured = {}; this.unitsLostValue = 0; this.enemyUnitsKilledValue = 0; this.unitsCapturedValue = 0; for (let counterName of ["unitsTrained", "unitsLost", "enemyUnitsKilled", "unitsCaptured"]) { this[counterName].total = 0; for (let unitClass of this.unitsClasses) // Domestic units are only counted for training if (unitClass != "Domestic" || counterName == "unitsTrained") this[counterName][unitClass] = 0; } this.buildingsConstructed = {}; this.buildingsLost = {}; this.enemyBuildingsDestroyed = {}; this.buildingsCaptured = {}; this.buildingsLostValue = 0; this.enemyBuildingsDestroyedValue = 0; this.buildingsCapturedValue = 0; for (let counterName of ["buildingsConstructed", "buildingsLost", "enemyBuildingsDestroyed", "buildingsCaptured"]) { this[counterName].total = 0; for (let unitClass of this.buildingsClasses) this[counterName][unitClass] = 0; } this.resourcesGathered = { "vegetarianFood": 0 }; this.resourcesUsed = {}; this.resourcesSold = {}; this.resourcesBought = {}; for (let res of Resources.GetCodes()) { this.resourcesGathered[res] = 0; this.resourcesUsed[res] = 0; this.resourcesSold[res] = 0; this.resourcesBought[res] = 0; } this.tributesSent = 0; this.tributesReceived = 0; this.tradeIncome = 0; this.treasuresCollected = 0; this.lootCollected = 0; this.peakPercentMapControlled = 0; this.teamPeakPercentMapControlled = 0; this.successfulBribes = 0; this.failedBribes = 0; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.updateTimer = cmpTimer.SetInterval( this.entity, IID_StatisticsTracker, "UpdateSequences", 0, this.UpdateSequenceInterval); }; StatisticsTracker.prototype.OnGlobalInitGame = function() { this.sequences = clone(this.GetStatistics()); this.sequences.time = []; }; /** * Returns a subset of statistics that will be added to the simulation state, * thus called each turn. Basic statistics should not contain data that would * be expensive to compute. * * Note: as of now, nothing in the game needs that, but some AIs developed by * modders need it in the API. */ StatisticsTracker.prototype.GetBasicStatistics = function() { return { "resourcesGathered": this.resourcesGathered, "percentMapExplored": this.GetPercentMapExplored() }; }; StatisticsTracker.prototype.GetStatistics = function() { return { "unitsTrained": this.unitsTrained, "unitsLost": this.unitsLost, "unitsLostValue": this.unitsLostValue, "enemyUnitsKilled": this.enemyUnitsKilled, "enemyUnitsKilledValue": this.enemyUnitsKilledValue, "unitsCaptured": this.unitsCaptured, "unitsCapturedValue": this.unitsCapturedValue, "buildingsConstructed": this.buildingsConstructed, "buildingsLost": this.buildingsLost, "buildingsLostValue": this.buildingsLostValue, "enemyBuildingsDestroyed": this.enemyBuildingsDestroyed, "enemyBuildingsDestroyedValue": this.enemyBuildingsDestroyedValue, "buildingsCaptured": this.buildingsCaptured, "buildingsCapturedValue": this.buildingsCapturedValue, "resourcesCount": this.GetResourceCounts(), "resourcesGathered": this.resourcesGathered, "resourcesUsed": this.resourcesUsed, "resourcesSold": this.resourcesSold, "resourcesBought": this.resourcesBought, "tributesSent": this.tributesSent, "tributesReceived": this.tributesReceived, "tradeIncome": this.tradeIncome, "treasuresCollected": this.treasuresCollected, "lootCollected": this.lootCollected, "populationCount": this.GetPopulationCount(), "percentMapExplored": this.GetPercentMapExplored(), "teamPercentMapExplored": this.GetTeamPercentMapExplored(), "percentMapControlled": this.GetPercentMapControlled(), "teamPercentMapControlled": this.GetTeamPercentMapControlled(), "peakPercentMapControlled": this.peakPercentMapControlled, "teamPeakPercentMapControlled": this.teamPeakPercentMapControlled, "successfulBribes": this.successfulBribes, "failedBribes": this.failedBribes }; }; StatisticsTracker.prototype.GetSequences = function() { let ret = clone(this.sequences); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.time.push(cmpTimer.GetTime() / 1000); this.PushValue(this.GetStatistics(), ret); return ret; }; /** * Used to print statistics for non-visual autostart games. * @return The player's statistics as a JSON string beautified with some indentations. */ StatisticsTracker.prototype.GetStatisticsJSON = function() { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let playerStatistics = { "playerID": cmpPlayer.GetPlayerID(), "playerState": cmpPlayer.GetState(), "statistics": this.GetStatistics() }; return JSON.stringify(playerStatistics, null, "\t"); }; /** * Increments counter associated with certain entity/counter and type of given entity. * @param classes - The classes an entity has. * @param counter - the name of the counter to increment (e.g. "unitsTrained"). * @param type - the type of the counter (e.g. "workers"). */ StatisticsTracker.prototype.CounterIncrement = function(classes, counter, type) { if (!classes) return; if (classes.includes(type)) ++this[counter][type]; }; /** * Counts the total number of units trained as well as an individual count for * each unit type. Based on templates. */ StatisticsTracker.prototype.IncreaseTrainedUnitsCounter = function(trainedUnit) { const classes = Engine.QueryInterface(trainedUnit, IID_Identity)?.GetClassesList(); if (!classes) return; for (const type of this.unitsClasses) this.CounterIncrement(classes, "unitsTrained", type); if (!classes.includes("Domestic")) ++this.unitsTrained.total; }; /** * Counts the total number of buildings constructed as well as an individual count for * each building type. Based on templates. */ StatisticsTracker.prototype.IncreaseConstructedBuildingsCounter = function(constructedBuilding) { const classes = Engine.QueryInterface(constructedBuilding, IID_Identity)?.GetClassesList(); if (!classes) return; for (const type of this.buildingsClasses) this.CounterIncrement(classes, "buildingsConstructed", type); ++this.buildingsConstructed.total; }; StatisticsTracker.prototype.KilledEntity = function(targetEntity) { const cmpTargetEntityIdentity = Engine.QueryInterface(targetEntity, IID_Identity); if (!cmpTargetEntityIdentity) return; const classes = cmpTargetEntityIdentity.GetClassesList(); const costs = Engine.QueryInterface(targetEntity, IID_Cost)?.GetResourceCosts(); if (cmpTargetEntityIdentity.HasClass("Unit") && !cmpTargetEntityIdentity.HasClass("Animal")) { for (const type of this.unitsClasses) this.CounterIncrement(classes, "enemyUnitsKilled", type); if (costs) for (const type in costs) this.enemyUnitsKilledValue += costs[type]; } if (cmpTargetEntityIdentity.HasClass("Structure") && !Engine.QueryInterface(targetEntity, IID_Foundation)) { for (const type of this.buildingsClasses) this.CounterIncrement(classes, "enemyBuildingsDestroyed", type); if (costs) for (const type in costs) this.enemyBuildingsDestroyedValue += costs[type]; } }; StatisticsTracker.prototype.LostEntity = function(lostEntity) { const cmpLostEntityIdentity = Engine.QueryInterface(lostEntity, IID_Identity); if (!cmpLostEntityIdentity) return; const classes = cmpLostEntityIdentity.GetClassesList(); const costs = Engine.QueryInterface(lostEntity, IID_Cost)?.GetResourceCosts(); if (cmpLostEntityIdentity.HasClass("Unit") && !cmpLostEntityIdentity.HasClass("Domestic")) { for (const type of this.unitsClasses) this.CounterIncrement(classes, "unitsLost", type); if (costs) for (const type in costs) this.unitsLostValue += costs[type]; } if (cmpLostEntityIdentity.HasClass("Structure") && !Engine.QueryInterface(lostEntity, IID_Foundation)) { for (const type of this.buildingsClasses) this.CounterIncrement(classes, "buildingsLost", type); if (costs) for (const type in costs) this.buildingsLostValue += costs[type]; } }; StatisticsTracker.prototype.CapturedEntity = function(capturedEntity) { const cmpCapturedEntityIdentity = Engine.QueryInterface(capturedEntity, IID_Identity); if (!cmpCapturedEntityIdentity) return; const classes = cmpCapturedEntityIdentity.GetClassesList(); const costs = Engine.QueryInterface(capturedEntity, IID_Cost)?.GetResourceCosts(); if (cmpCapturedEntityIdentity.HasClass("Unit")) { for (const type of this.unitsClasses) this.CounterIncrement(classes, "unitsCaptured", type); if (costs) for (const type in costs) this.unitsCapturedValue += costs[type]; } if (cmpCapturedEntityIdentity.HasClass("Structure")) { for (const type of this.buildingsClasses) this.CounterIncrement(classes, "buildingsCaptured", type); if (costs) for (const type in costs) this.buildingsCapturedValue += costs[type]; } }; /** * @return {Object} - The amount of available resources. */ StatisticsTracker.prototype.GetResourceCounts = function() { return Engine.QueryInterface(this.entity, IID_Player)?.GetResourceCounts() ?? Object.fromEntries(Resources.GetCodes().map(res => [res, 0])); }; /** * @param {string} type - generic type of resource. * @param {number} amount - amount of resource, whick should be added. * @param {string} specificType - specific type of resource. */ StatisticsTracker.prototype.IncreaseResourceGatheredCounter = function(type, amount, specificType) { this.resourcesGathered[type] += amount; if (type == "food" && (specificType == "fruit" || specificType == "grain")) this.resourcesGathered.vegetarianFood += amount; }; /** * @param {string} type - generic type of resource. * @param {number} amount - amount of resource, which should be added. */ StatisticsTracker.prototype.IncreaseResourceUsedCounter = function(type, amount) { this.resourcesUsed[type] += amount; }; StatisticsTracker.prototype.IncreaseTreasuresCollectedCounter = function() { ++this.treasuresCollected; }; StatisticsTracker.prototype.IncreaseLootCollectedCounter = function(amount) { for (let type in amount) this.lootCollected += amount[type]; }; StatisticsTracker.prototype.IncreaseResourcesSoldCounter = function(type, amount) { this.resourcesSold[type] += amount; }; StatisticsTracker.prototype.IncreaseResourcesBoughtCounter = function(type, amount) { this.resourcesBought[type] += amount; }; StatisticsTracker.prototype.IncreaseTributesSentCounter = function(amount) { this.tributesSent += amount; }; StatisticsTracker.prototype.IncreaseTributesReceivedCounter = function(amount) { this.tributesReceived += amount; }; StatisticsTracker.prototype.IncreaseTradeIncomeCounter = function(amount) { this.tradeIncome += amount; }; StatisticsTracker.prototype.GetPopulationCount = function() { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); return cmpPlayer ? cmpPlayer.GetPopulationCount() : 0; }; StatisticsTracker.prototype.IncreaseSuccessfulBribesCounter = function() { ++this.successfulBribes; }; StatisticsTracker.prototype.IncreaseFailedBribesCounter = function() { ++this.failedBribes; }; StatisticsTracker.prototype.GetPercentMapExplored = function() { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) return 0; return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetPercentMapExplored(cmpPlayer.GetPlayerID()); }; /** * Note: cmpRangeManager.GetUnionPercentMapExplored computes statistics from scratch! * As a consequence, this function should not be called too often. */ StatisticsTracker.prototype.GetTeamPercentMapExplored = function() { - let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); - if (!cmpPlayer) + const cmpDiplomacy = Engine.QueryInterface(this.entity, IID_Diplomacy); + if (!cmpDiplomacy) return 0; - let team = cmpPlayer.GetTeam(); - let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + const team = cmpDiplomacy.GetTeam(); + const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); // If teams are not locked, this statistic won't be displayed, so don't bother computing - if (team == -1 || !cmpPlayer.GetLockTeams()) + if (team == -1 || !cmpDiplomacy.IsTeamLocked()) + { + const cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + if (!cmpPlayer) + return 0; return cmpRangeManager.GetPercentMapExplored(cmpPlayer.GetPlayerID()); + } - let teamPlayers = []; - let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + const teamPlayers = []; + const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) - { - let cmpOtherPlayer = QueryPlayerIDInterface(i); - if (cmpOtherPlayer && cmpOtherPlayer.GetTeam() == team) + if (QueryPlayerIDInterface(i, IID_Diplomacy)?.GetTeam() === team) teamPlayers.push(i); - } return cmpRangeManager.GetUnionPercentMapExplored(teamPlayers); }; StatisticsTracker.prototype.GetPercentMapControlled = function() { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) return 0; return Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).GetTerritoryPercentage(cmpPlayer.GetPlayerID()); }; StatisticsTracker.prototype.GetTeamPercentMapControlled = function() { - let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); - if (!cmpPlayer) + const cmpDiplomacy = Engine.QueryInterface(this.entity, IID_Diplomacy); + if (!cmpDiplomacy) return 0; - let team = cmpPlayer.GetTeam(); - let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); - if (team == -1 || !cmpPlayer.GetLockTeams()) + const team = cmpDiplomacy.GetTeam(); + const cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); + if (team === -1 || !cmpDiplomacy.IsTeamLocked()) + { + let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + if (!cmpPlayer) + return 0; return cmpTerritoryManager.GetTerritoryPercentage(cmpPlayer.GetPlayerID()); + } let teamPercent = 0; - let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) - { - let cmpOtherPlayer = QueryPlayerIDInterface(i); - if (cmpOtherPlayer && cmpOtherPlayer.GetTeam() == team) + if (QueryPlayerIDInterface(i, IID_Diplomacy)?.GetTeam() === team) teamPercent += cmpTerritoryManager.GetTerritoryPercentage(i); - } return teamPercent; }; StatisticsTracker.prototype.OnTerritoriesChanged = function(msg) { this.UpdatePeakPercentages(); }; StatisticsTracker.prototype.OnGlobalPlayerDefeated = function(msg) { this.UpdatePeakPercentages(); }; StatisticsTracker.prototype.OnGlobalPlayerWon = function(msg) { this.UpdatePeakPercentages(); }; StatisticsTracker.prototype.UpdatePeakPercentages = function() { this.peakPercentMapControlled = Math.max(this.peakPercentMapControlled, this.GetPercentMapControlled()); this.teamPeakPercentMapControlled = Math.max(this.teamPeakPercentMapControlled, this.GetTeamPercentMapControlled()); }; /** * Adds the values of fromData to the end of the arrays of toData. * If toData misses the needed array, one will be created. * * @param fromData - an object of values or a value. * @param toData - an object of arrays or an array. **/ StatisticsTracker.prototype.PushValue = function(fromData, toData) { if (typeof fromData == "object") for (let prop in fromData) { if (typeof toData[prop] != "object") toData[prop] = [fromData[prop]]; else this.PushValue(fromData[prop], toData[prop]); } else toData.push(fromData); }; StatisticsTracker.prototype.UpdateSequences = function() { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.sequences.time.push(cmpTimer.GetTime() / 1000); this.PushValue(this.GetStatistics(), this.sequences); }; Engine.RegisterComponentType(IID_StatisticsTracker, "StatisticsTracker", StatisticsTracker); Index: ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js (revision 27722) @@ -1,171 +1,176 @@ function TerritoryDecay() {} TerritoryDecay.prototype.Schema = ` Infinity neutral enemy `; TerritoryDecay.prototype.Init = function() { this.decaying = false; this.connectedNeighbours = new Array(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers()).fill(0); this.territoryOwnership = !isFinite(+this.template.DecayRate); }; TerritoryDecay.prototype.IsConnected = function() { this.connectedNeighbours.fill(0); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; var cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return true;// something without ownership can't decay + const playerID = cmpPlayer.GetPlayerID(); + const cmpDiplomacy = QueryPlayerIDInterface(playerID, IID_Diplomacy); + if (!cmpDiplomacy) + return true; + const decayTerritory = ApplyValueModificationsToEntity("TerritoryDecay/Territory", this.template.Territory, this.entity); var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); var pos = cmpPosition.GetPosition2D(); var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); if (tileOwner == 0) { this.connectedNeighbours[0] = 1; - return cmpPlayer.GetPlayerID() == 0 || decayTerritory.indexOf("neutral") === -1; + return playerID == 0 || decayTerritory.indexOf("neutral") === -1; } var tileConnected = cmpTerritoryManager.IsConnected(pos.x, pos.y); - if (tileConnected && !cmpPlayer.IsMutualAlly(tileOwner)) + if (tileConnected && !cmpDiplomacy.IsMutualAlly(tileOwner)) { this.connectedNeighbours[tileOwner] = 1; return decayTerritory.indexOf("enemy") === -1; } if (tileConnected) return true; // Special-case: if the tile is unconnected, non-own territory, decay towards gaia. // TODO: this is not great, see #4749 - if (cmpPlayer.GetPlayerID() != tileOwner) + if (playerID != tileOwner) { this.connectedNeighbours[0] = 1; return false; } this.connectedNeighbours = cmpTerritoryManager.GetNeighbours(pos.x, pos.y, true); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) - if (this.connectedNeighbours[i] > 0 && cmpPlayer.IsMutualAlly(i)) + if (this.connectedNeighbours[i] > 0 && cmpDiplomacy.IsMutualAlly(i)) { // don't decay if connected to a connected ally; disable blinking cmpTerritoryManager.SetTerritoryBlinking(pos.x, pos.y, false); return true; } cmpTerritoryManager.SetTerritoryBlinking(pos.x, pos.y, true); return false; }; TerritoryDecay.prototype.IsDecaying = function() { return this.decaying; }; TerritoryDecay.prototype.GetDecayRate = function() { return ApplyValueModificationsToEntity( "TerritoryDecay/DecayRate", +this.template.DecayRate, this.entity); }; /** * Get the number of connected bordering tiles to this region * Only valid when this.IsDecaying() */ TerritoryDecay.prototype.GetConnectedNeighbours = function() { return this.connectedNeighbours; }; TerritoryDecay.prototype.UpdateDecayState = function() { let decaying = !this.IsConnected() && this.GetDecayRate() > 0; if (decaying === this.decaying) return; this.decaying = decaying; Engine.PostMessage(this.entity, MT_TerritoryDecayChanged, { "entity": this.entity, "to": decaying, "rate": this.GetDecayRate() }); }; TerritoryDecay.prototype.UpdateOwner = function() { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpOwnership || !cmpPosition || !cmpPosition.IsInWorld()) return; let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); let pos = cmpPosition.GetPosition2D(); let tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); if (tileOwner != cmpOwnership.GetOwner()) cmpOwnership.SetOwner(tileOwner); }; TerritoryDecay.prototype.OnTerritoriesChanged = function(msg) { if (this.territoryOwnership) this.UpdateOwner(); else this.UpdateDecayState(); }; TerritoryDecay.prototype.OnPositionChanged = function(msg) { if (this.territoryOwnership) this.UpdateOwner(); else this.UpdateDecayState(); }; TerritoryDecay.prototype.OnDiplomacyChanged = function(msg) { // Can change the connectedness of certain areas if (!this.territoryOwnership) this.UpdateDecayState(); }; TerritoryDecay.prototype.OnOwnershipChanged = function(msg) { // Update the list of TerritoryDecay components in the manager if (msg.from == INVALID_PLAYER || msg.to == INVALID_PLAYER) { let cmpTerritoryDecayManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryDecayManager); if (msg.from == INVALID_PLAYER) cmpTerritoryDecayManager.Add(this.entity); else cmpTerritoryDecayManager.Remove(this.entity); } // if it influences the territory, wait until we get a TerritoriesChanged message if (!this.territoryOwnership && !Engine.QueryInterface(this.entity, IID_TerritoryInfluence)) this.UpdateDecayState(); }; TerritoryDecay.prototype.HasTerritoryOwnership = function() { return this.territoryOwnership; }; Engine.RegisterComponentType(IID_TerritoryDecay, "TerritoryDecay", TerritoryDecay); Index: ps/trunk/binaries/data/mods/public/simulation/components/Trader.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Trader.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/Trader.js (revision 27722) @@ -1,310 +1,310 @@ // See helpers/TraderGain.js for the CalculateTaderGain() function which works out how many // resources a trader gets function Trader() {} Trader.prototype.Schema = "Lets the unit generate resources while moving between markets (or docks in case of water trading)." + "" + "0.75" + "0.2" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Trader.prototype.Init = function() { this.markets = []; this.index = -1; this.goods = { "type": null, "amount": null }; }; Trader.prototype.CalculateGain = function(currentMarket, nextMarket) { let cmpMarket = QueryMiragedInterface(currentMarket, IID_Market); let gain = cmpMarket && cmpMarket.CalculateTraderGain(nextMarket, this.template, this.entity); if (!gain) // One of our markets must have been destroyed return null; // For garrisonable unit increase gain for each garrisoned trader // Calculate this here to save passing unnecessary stuff into the CalculateTraderGain function let garrisonGainMultiplier = this.GetGarrisonGainMultiplier(); if (garrisonGainMultiplier === undefined) return gain; let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder) return gain; let garrisonMultiplier = 1; let garrisonedTradersCount = 0; for (let entity of cmpGarrisonHolder.GetEntities()) { let cmpGarrisonedUnitTrader = Engine.QueryInterface(entity, IID_Trader); if (cmpGarrisonedUnitTrader) ++garrisonedTradersCount; } garrisonMultiplier *= 1 + garrisonGainMultiplier * garrisonedTradersCount; if (gain.traderGain) gain.traderGain = Math.round(garrisonMultiplier * gain.traderGain); if (gain.market1Gain) gain.market1Gain = Math.round(garrisonMultiplier * gain.market1Gain); if (gain.market2Gain) gain.market2Gain = Math.round(garrisonMultiplier * gain.market2Gain); return gain; }; /** * Remove market from trade route iff only first market is set. * @param {number} id of market to be removed. * @return {boolean} true iff removal was successful. */ Trader.prototype.RemoveTargetMarket = function(target) { if (this.markets.length != 1 || this.markets[0] != target) return false; let cmpTargetMarket = QueryMiragedInterface(target, IID_Market); if (!cmpTargetMarket) return false; cmpTargetMarket.RemoveTrader(this.entity); this.index = -1; this.markets = []; return true; }; // Set target as target market. // Return true if at least one of markets was changed. Trader.prototype.SetTargetMarket = function(target, source) { let cmpTargetMarket = QueryMiragedInterface(target, IID_Market); if (!cmpTargetMarket) return false; if (source) { // Establish a trade route with both markets in one go. let cmpSourceMarket = QueryMiragedInterface(source, IID_Market); if (!cmpSourceMarket) return false; this.markets = [source]; } if (this.markets.length >= 2) { // If we already have both markets - drop them // and use the target as first market for (let market of this.markets) { let cmpMarket = QueryMiragedInterface(market, IID_Market); if (cmpMarket) cmpMarket.RemoveTrader(this.entity); } this.index = 0; this.markets = [target]; cmpTargetMarket.AddTrader(this.entity); } else if (this.markets.length == 1) { // If we have only one market and target is different from it, // set the target as second one if (target == this.markets[0]) return false; this.index = 0; this.markets.push(target); cmpTargetMarket.AddTrader(this.entity); this.goods.amount = this.CalculateGain(this.markets[0], this.markets[1]); } else { // Else we don't have target markets at all, // set the target as first market this.index = 0; this.markets = [target]; cmpTargetMarket.AddTrader(this.entity); } // Drop carried goods if markets were changed this.goods.amount = null; return true; }; Trader.prototype.GetFirstMarket = function() { return this.markets[0] || null; }; Trader.prototype.GetSecondMarket = function() { return this.markets[1] || null; }; Trader.prototype.GetTraderGainMultiplier = function() { return ApplyValueModificationsToEntity("Trader/GainMultiplier", +this.template.GainMultiplier, this.entity); }; Trader.prototype.GetGarrisonGainMultiplier = function() { if (this.template.GarrisonGainMultiplier === undefined) return undefined; return ApplyValueModificationsToEntity("Trader/GarrisonGainMultiplier", +this.template.GarrisonGainMultiplier, this.entity); }; Trader.prototype.HasBothMarkets = function() { return this.markets.length >= 2; }; Trader.prototype.CanTrade = function(target) { let cmpTraderIdentity = Engine.QueryInterface(this.entity, IID_Identity); let cmpTargetMarket = QueryMiragedInterface(target, IID_Market); if (!cmpTargetMarket) return false; let cmpTargetFoundation = Engine.QueryInterface(target, IID_Foundation); if (cmpTargetFoundation) return false; if (!(cmpTraderIdentity.HasClass("Organic") && cmpTargetMarket.HasType("land")) && !(cmpTraderIdentity.HasClass("Ship") && cmpTargetMarket.HasType("naval"))) return false; - let cmpTraderPlayer = QueryOwnerInterface(this.entity, IID_Player); + let cmpTraderDiplomacy = QueryOwnerInterface(this.entity, IID_Diplomacy); let cmpTargetPlayer = QueryOwnerInterface(target, IID_Player); - return cmpTraderPlayer && cmpTargetPlayer && !cmpTraderPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()); + return cmpTraderPlayer && cmpTargetPlayer && !cmpTraderDiplomacy.IsEnemy(cmpTargetPlayer.GetPlayerID()); }; Trader.prototype.AddResources = function(ent, gain) { let cmpPlayer = QueryOwnerInterface(ent); if (cmpPlayer) cmpPlayer.AddResource(this.goods.type, gain); let cmpStatisticsTracker = QueryOwnerInterface(ent, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseTradeIncomeCounter(gain); }; Trader.prototype.GenerateResources = function(currentMarket, nextMarket) { this.AddResources(this.entity, this.goods.amount.traderGain); if (this.goods.amount.market1Gain) this.AddResources(currentMarket, this.goods.amount.market1Gain); if (this.goods.amount.market2Gain) this.AddResources(nextMarket, this.goods.amount.market2Gain); }; Trader.prototype.PerformTrade = function(currentMarket) { let previousMarket = this.markets[this.index]; if (previousMarket != currentMarket) // Inconsistent markets { this.goods.amount = null; return INVALID_ENTITY; } this.index = ++this.index % this.markets.length; let nextMarket = this.markets[this.index]; if (this.goods.amount && this.goods.amount.traderGain) this.GenerateResources(previousMarket, nextMarket); let cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return INVALID_ENTITY; this.goods.type = cmpPlayer.GetNextTradingGoods(); this.goods.amount = this.CalculateGain(currentMarket, nextMarket); return nextMarket; }; Trader.prototype.GetGoods = function() { return this.goods; }; /** * Returns true if the trader has the given market (can be either a market or a mirage) */ Trader.prototype.HasMarket = function(market) { return this.markets.indexOf(market) != -1; }; /** * Remove a market when this trader can no longer trade with it */ Trader.prototype.RemoveMarket = function(market) { let index = this.markets.indexOf(market); if (index == -1) return; this.markets.splice(index, 1); let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.MarketRemoved(market); }; /** * Switch between a market and its mirage according to visibility */ Trader.prototype.SwitchMarket = function(oldMarket, newMarket) { let index = this.markets.indexOf(oldMarket); if (index == -1) return; this.markets[index] = newMarket; let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SwitchMarketOrder(oldMarket, newMarket); }; Trader.prototype.StopTrading = function() { for (let market of this.markets) { let cmpMarket = QueryMiragedInterface(market, IID_Market); if (cmpMarket) cmpMarket.RemoveTrader(this.entity); } this.index = -1; this.markets = []; this.goods.amount = null; this.markets = []; }; // Get range in which deals with market are available, // i.e. trader should be in no more than MaxDistance from market // to be able to trade with it. Trader.prototype.GetRange = function() { let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); let max = 1; if (cmpObstruction) max += cmpObstruction.GetSize() * 1.5; return { "min": 0, "max": max }; }; Trader.prototype.OnGarrisonedUnitsChanged = function() { if (this.HasBothMarkets()) this.goods.amount = this.CalculateGain(this.markets[0], this.markets[1]); }; Engine.RegisterComponentType(IID_Trader, "Trader", Trader); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 27722) @@ -1,6530 +1,6527 @@ function UnitAI() {} UnitAI.prototype.Schema = "Controls the unit's movement, attacks, etc, in response to commands from the player." + "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "standground" + "skittish" + "passive-defensive" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + ""; // Unit stances. // There some targeting options: // targetVisibleEnemies: anything in vision range is a viable target // targetAttackersAlways: anything that hurts us is a viable target, // possibly overriding user orders! // There are some response options, triggered when targets are detected: // respondFlee: run away // respondFleeOnSight: run away when an enemy is sighted // respondChase: start chasing after the enemy // respondChaseBeyondVision: start chasing, and don't stop even if it's out // of this unit's vision range (though still visible to the player) // respondStandGround: attack enemy but don't move at all // respondHoldGround: attack enemy but don't move far from current position // TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts, // do worry around armies slaughtering the guy standing next to you), etc. var g_Stances = { "violent": { "targetVisibleEnemies": true, "targetAttackersAlways": true, "respondFlee": false, "respondFleeOnSight": false, "respondChase": true, "respondChaseBeyondVision": true, "respondStandGround": false, "respondHoldGround": false, "selectable": true }, "aggressive": { "targetVisibleEnemies": true, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": true, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": false, "selectable": true }, "defensive": { "targetVisibleEnemies": true, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": true, "selectable": true }, "passive": { "targetVisibleEnemies": false, "targetAttackersAlways": false, "respondFlee": true, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": false, "selectable": true }, "standground": { "targetVisibleEnemies": true, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": true, "respondHoldGround": false, "selectable": true }, "skittish": { "targetVisibleEnemies": false, "targetAttackersAlways": false, "respondFlee": true, "respondFleeOnSight": true, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": false, "selectable": false }, "passive-defensive": { "targetVisibleEnemies": false, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": true, "selectable": false }, "none": { // Only to be used by AI or trigger scripts "targetVisibleEnemies": false, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": false, "selectable": false } }; // These orders always require a packed unit, so if a unit that is unpacking is given one of these orders, // it will immediately cancel unpacking. var g_OrdersCancelUnpacking = new Set([ "FormationWalk", "Walk", "WalkAndFight", "WalkToTarget", "Patrol", "Garrison" ]); // When leaving a foundation, we want to be clear of it by this distance. var g_LeaveFoundationRange = 4; UnitAI.prototype.notifyToCheerInRange = 30; UnitAI.prototype.DEFAULT_CAPTURE = false; // To reject an order, use 'return this.FinishOrder();' const ACCEPT_ORDER = true; // See ../helpers/FSM.js for some documentation of this FSM specification syntax UnitAI.prototype.UnitFsmSpec = { // Default event handlers: "MovementUpdate": function(msg) { // ignore spurious movement messages // (these can happen when stopping moving at the same time // as switching states) }, "ConstructionFinished": function(msg) { // ignore uninteresting construction messages }, "LosRangeUpdate": function(msg) { // Ignore newly-seen units by default. }, "LosHealRangeUpdate": function(msg) { // Ignore newly-seen injured units by default. }, "LosAttackRangeUpdate": function(msg) { // Ignore newly-seen enemy units by default. }, "Attacked": function(msg) { // ignore attacker }, "PackFinished": function(msg) { // ignore }, "PickupCanceled": function(msg) { // ignore }, "TradingCanceled": function(msg) { // ignore }, "GuardedAttacked": function(msg) { // ignore }, "OrderTargetRenamed": function() { // By default, trigger an exit-reenter // so that state preconditions are checked against the new entity // (there is no reason to assume the target is still valid). this.SetNextState(this.GetCurrentState()); }, // Formation handlers: "FormationLeave": function(msg) { // Overloaded by FORMATIONMEMBER // We end up here if LeaveFormation was called when the entity // was executing an order in an individual state, so we must // discard the order now that it has been executed. if (this.order && this.order.type === "LeaveFormation") this.FinishOrder(); }, // Called when being told to walk as part of a formation "Order.FormationWalk": function(msg) { if (!this.IsFormationMember() || !this.AbleToMove()) return this.FinishOrder(); if (this.CanPack()) { // If the controller is IDLE, this is just the regular reformation timer. // In that case we don't actually want to move, as that would unpack us. let cmpControllerAI = Engine.QueryInterface(this.GetFormationController(), IID_UnitAI); if (cmpControllerAI.IsIdle()) return this.FinishOrder(); this.PushOrderFront("Pack", { "force": true }); } else this.SetNextState("FORMATIONMEMBER.WALKING"); return ACCEPT_ORDER; }, // Special orders: // (these will be overridden by various states) "Order.LeaveFoundation": function(msg) { if (!this.WillMoveFromFoundation(msg.data.target)) return this.FinishOrder(); msg.data.min = g_LeaveFoundationRange; this.SetNextState("INDIVIDUAL.WALKING"); return ACCEPT_ORDER; }, // Individual orders: "Order.LeaveFormation": function() { if (!this.IsFormationMember()) return this.FinishOrder(); let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { cmpFormation.SetRearrange(false); // Triggers FormationLeave, which ultimately will FinishOrder, // discarding this order. cmpFormation.RemoveMembers([this.entity]); cmpFormation.SetRearrange(true); } return ACCEPT_ORDER; }, "Order.Stop": function(msg) { this.FinishOrder(); return ACCEPT_ORDER; }, "Order.Walk": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } this.SetHeldPosition(msg.data.x, msg.data.z); msg.data.relaxed = true; this.SetNextState("INDIVIDUAL.WALKING"); return ACCEPT_ORDER; }, "Order.WalkAndFight": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } this.SetHeldPosition(msg.data.x, msg.data.z); msg.data.relaxed = true; this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING"); return ACCEPT_ORDER; }, "Order.WalkToTarget": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } if (this.CheckRange(msg.data)) return this.FinishOrder(); msg.data.relaxed = true; this.SetNextState("INDIVIDUAL.WALKING"); return ACCEPT_ORDER; }, "Order.PickupUnit": function(msg) { let cmpHolder = Engine.QueryInterface(this.entity, msg.data.iid); if (!cmpHolder || cmpHolder.IsFull()) return this.FinishOrder(); let range = cmpHolder.LoadingRange(); msg.data.min = range.min; msg.data.max = range.max; if (this.CheckRange(msg.data)) return this.FinishOrder(); // Check if we need to move // If the target can reach us and we are reasonably close, don't move. // TODO: it would be slightly more optimal to check for real, not bird-flight distance. let cmpPassengerMotion = Engine.QueryInterface(msg.data.target, IID_UnitMotion); if (cmpPassengerMotion && cmpPassengerMotion.IsTargetRangeReachable(this.entity, range.min, range.max) && PositionHelper.DistanceBetweenEntities(this.entity, msg.data.target) < 200) this.SetNextState("INDIVIDUAL.PICKUP.LOADING"); else if (this.AbleToMove()) this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING"); else return this.FinishOrder(); return ACCEPT_ORDER; }, "Order.Guard": function(msg) { if (!this.AddGuard(msg.data.target)) return this.FinishOrder(); if (this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("INDIVIDUAL.GUARD.GUARDING"); else if (this.AbleToMove()) this.SetNextState("INDIVIDUAL.GUARD.ESCORTING"); else return this.FinishOrder(); return ACCEPT_ORDER; }, "Order.Flee": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); this.SetNextState("INDIVIDUAL.FLEEING"); return ACCEPT_ORDER; }, "Order.Attack": function(msg) { let type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture); if (!type) return this.FinishOrder(); msg.data.attackType = type; this.RememberTargetPosition(); if (msg.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); if (this.CheckTargetAttackRange(msg.data.target, msg.data.attackType)) { if (this.CanUnpack()) { this.PushOrderFront("Unpack", { "force": true }); return ACCEPT_ORDER; } // Cancel any current packing order. if (this.EnsureCorrectPackStateForAttack(false)) this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); return ACCEPT_ORDER; } // If we're hunting, that's a special case where we should continue attacking our target. if (this.GetStance().respondStandGround && !msg.data.force && !msg.data.hunting || !this.AbleToMove()) return this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } // If we're currently packing/unpacking, make sure we are packed, so we can move. if (this.EnsureCorrectPackStateForAttack(true)) this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); return ACCEPT_ORDER; }, "Order.Patrol": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } msg.data.relaxed = true; this.SetNextState("INDIVIDUAL.PATROL.PATROLLING"); return ACCEPT_ORDER; }, "Order.Heal": function(msg) { if (!this.TargetIsAlive(msg.data.target)) return this.FinishOrder(); // Healers can't heal themselves. if (msg.data.target == this.entity) return this.FinishOrder(); if (this.CheckTargetRange(msg.data.target, IID_Heal)) { this.SetNextState("INDIVIDUAL.HEAL.HEALING"); return ACCEPT_ORDER; } if (!this.AbleToMove()) return this.FinishOrder(); if (this.GetStance().respondStandGround && !msg.data.force) return this.FinishOrder(); this.SetNextState("INDIVIDUAL.HEAL.APPROACHING"); return ACCEPT_ORDER; }, "Order.Gather": function(msg) { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return this.FinishOrder(); // We were given the order to gather while we were still gathering. // This is needed because we don't re-enter the GATHER-state. const taskedResourceType = cmpResourceGatherer.GetTaskedResourceType(); if (taskedResourceType && msg.data.type.generic != taskedResourceType) this.UnitFsm.SwitchToNextState(this, "INDIVIDUAL.GATHER"); if (!this.CanGather(msg.data.target)) { this.SetNextState("INDIVIDUAL.GATHER.FINDINGNEWTARGET"); return ACCEPT_ORDER; } if (this.MustKillGatherTarget(msg.data.target)) { const bestAttack = Engine.QueryInterface(this.entity, IID_Attack)?.GetBestAttackAgainst(msg.data.target, false); // Make sure we can attack the target, else we'll get very stuck if (!bestAttack) { // Oops, we can't attack at all - give up // TODO: should do something so the player knows why this failed return this.FinishOrder(); } // The target was visible when this order was issued, // but could now be invisible again. if (!this.CheckTargetVisible(msg.data.target)) { if (msg.data.secondTry === undefined) { msg.data.secondTry = true; this.PushOrderFront("Walk", msg.data.lastPos); } // We couldn't move there, or the target moved away else if (!this.FinishOrder()) this.PushOrderFront("GatherNearPosition", { "x": msg.data.lastPos.x, "z": msg.data.lastPos.z, "type": msg.data.type, "template": msg.data.template }); return ACCEPT_ORDER; } if (!this.AbleToMove() && !this.CheckTargetRange(msg.data.target, IID_Attack, bestAttack)) return this.FinishOrder(); this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true }); return ACCEPT_ORDER; } // If the unit is full go to the nearest dropsite instead of trying to gather. if (!cmpResourceGatherer.CanCarryMore(msg.data.type.generic)) { this.SetNextState("INDIVIDUAL.GATHER.RETURNINGRESOURCE"); return ACCEPT_ORDER; } this.RememberTargetPosition(); if (!msg.data.initPos) msg.data.initPos = msg.data.lastPos; if (this.CheckTargetRange(msg.data.target, IID_ResourceGatherer)) this.SetNextState("INDIVIDUAL.GATHER.GATHERING"); else if (this.AbleToMove()) this.SetNextState("INDIVIDUAL.GATHER.APPROACHING"); else return this.FinishOrder(); return ACCEPT_ORDER; }, "Order.GatherNearPosition": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); this.SetNextState("INDIVIDUAL.GATHER.WALKING"); msg.data.initPos = { 'x': msg.data.x, 'z': msg.data.z }; msg.data.relaxed = true; return ACCEPT_ORDER; }, "Order.DropAtNearestDropSite": function(msg) { const cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return this.FinishOrder(); const nearby = this.FindNearestDropsite(cmpResourceGatherer.GetMainCarryingType()); if (!nearby) return this.FinishOrder(); this.ReturnResource(nearby, false, true); return ACCEPT_ORDER; }, "Order.ReturnResource": function(msg) { if (this.CheckTargetRange(msg.data.target, IID_ResourceGatherer)) this.SetNextState("INDIVIDUAL.RETURNRESOURCE.DROPPINGRESOURCES"); else if (this.AbleToMove()) this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING"); else return this.FinishOrder(); return ACCEPT_ORDER; }, "Order.Trade": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); // We must check if this trader has both markets in case it was a back-to-work order. let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader || !cmpTrader.HasBothMarkets()) return this.FinishOrder(); this.waypoints = []; this.SetNextState("TRADE.APPROACHINGMARKET"); return ACCEPT_ORDER; }, "Order.Repair": function(msg) { if (this.CheckTargetRange(msg.data.target, IID_Builder)) this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING"); else if (this.AbleToMove()) this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING"); else return this.FinishOrder(); return ACCEPT_ORDER; }, "Order.Garrison": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); // Also pack when we are in range. if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } if (this.CheckTargetRange(msg.data.target, msg.data.garrison ? IID_Garrisonable : IID_Turretable)) this.SetNextState("INDIVIDUAL.GARRISON.GARRISONING"); else this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); return ACCEPT_ORDER; }, "Order.Ungarrison": function(msg) { // Note that this order MUST succeed, or we break // the assumptions done in garrisonable/garrisonHolder, // especially in Unloading in the latter. (For user feedback.) // ToDo: This can be fixed by not making that assumption :) this.FinishOrder(); return ACCEPT_ORDER; }, "Order.Cheer": function(msg) { return this.FinishOrder(); }, "Order.Pack": function(msg) { if (!this.CanPack()) return this.FinishOrder(); this.SetNextState("INDIVIDUAL.PACKING"); return ACCEPT_ORDER; }, "Order.Unpack": function(msg) { if (!this.CanUnpack()) return this.FinishOrder(); this.SetNextState("INDIVIDUAL.UNPACKING"); return ACCEPT_ORDER; }, "Order.MoveToChasingPoint": function(msg) { // Overriden by the CHASING state. // Can however happen outside of it when renaming... // TODO: don't use an order for that behaviour. return this.FinishOrder(); }, "Order.CollectTreasure": function(msg) { if (this.CheckTargetRange(msg.data.target, IID_TreasureCollector)) this.SetNextState("INDIVIDUAL.COLLECTTREASURE.COLLECTING"); else if (this.AbleToMove()) this.SetNextState("INDIVIDUAL.COLLECTTREASURE.APPROACHING"); else return this.FinishOrder(); return ACCEPT_ORDER; }, "Order.CollectTreasureNearPosition": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); this.SetNextState("INDIVIDUAL.COLLECTTREASURE.WALKING"); msg.data.initPos = { 'x': msg.data.x, 'z': msg.data.z }; msg.data.relaxed = true; return ACCEPT_ORDER; }, // States for the special entity representing a group of units moving in formation: "FORMATIONCONTROLLER": { "Order.Walk": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.WalkAndFight": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.SetNextState("WALKINGANDFIGHTING"); return ACCEPT_ORDER; }, "Order.MoveIntoFormation": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.SetNextState("FORMING"); return ACCEPT_ORDER; }, // Only used by other orders to walk there in formation. "Order.WalkToTargetRange": function(msg) { if (this.CheckRange(msg.data)) return this.FinishOrder(); if (!this.AbleToMove()) return this.FinishOrder(); this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.WalkToTarget": function(msg) { if (this.CheckRange(msg.data)) return this.FinishOrder(); if (!this.AbleToMove()) return this.FinishOrder(); this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.WalkToPointRange": function(msg) { if (this.CheckRange(msg.data)) return this.FinishOrder(); if (!this.AbleToMove()) return this.FinishOrder(); this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.Patrol": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.SetNextState("PATROL.PATROLLING"); return ACCEPT_ORDER; }, "Order.Guard": function(msg) { this.CallMemberFunction("Guard", [msg.data.target, false]); Engine.QueryInterface(this.entity, IID_Formation).Disband(); return ACCEPT_ORDER; }, "Order.Stop": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.ResetOrderVariant(); if (!this.IsAttackingAsFormation()) this.CallMemberFunction("Stop", [false]); this.FinishOrder(); return ACCEPT_ORDER; // Don't move the members back into formation, // as the formation then resets and it looks odd when walk-stopping. // TODO: this should be improved in the formation reshaping code. }, "Order.Attack": function(msg) { let target = msg.data.target; let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) target = cmpTargetUnitAI.GetFormationController(); if (!this.CheckFormationTargetAttackRange(target)) { if (this.AbleToMove() && this.CheckTargetVisible(target)) { this.SetNextState("COMBAT.APPROACHING"); return ACCEPT_ORDER; } return this.FinishOrder(); } this.CallMemberFunction("Attack", [target, msg.data.allowCapture, false]); let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (cmpAttack && cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Garrison": function(msg) { if (!Engine.QueryInterface(msg.data.target, msg.data.garrison ? IID_GarrisonHolder : IID_TurretHolder)) return this.FinishOrder(); if (this.CheckTargetRange(msg.data.target, msg.data.garrison ? IID_Garrisonable : IID_Turretable)) { if (!this.AbleToMove() || !this.CheckTargetVisible(msg.data.target)) return this.FinishOrder(); this.SetNextState("GARRISON.APPROACHING"); } else this.SetNextState("GARRISON.GARRISONING"); return ACCEPT_ORDER; }, "Order.Gather": function(msg) { if (this.MustKillGatherTarget(msg.data.target)) { // The target was visible when this order was given, // but could now be invisible. if (!this.CheckTargetVisible(msg.data.target)) { if (msg.data.secondTry === undefined) { msg.data.secondTry = true; this.PushOrderFront("Walk", msg.data.lastPos); } // We couldn't move there, or the target moved away else { let data = msg.data; if (!this.FinishOrder()) this.PushOrderFront("GatherNearPosition", { "x": data.lastPos.x, "z": data.lastPos.z, "type": data.type, "template": data.template }); } return ACCEPT_ORDER; } this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "min": 0, "max": 10 }); return ACCEPT_ORDER; } // TODO: on what should we base this range? if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) return this.FinishOrder(); // TODO: Should we issue a gather-near-position order // if the target isn't gatherable/doesn't exist anymore? if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return this.FinishOrder(); } this.CallMemberFunction("Gather", [msg.data.target, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.GatherNearPosition": function(msg) { // TODO: on what should we base this range? if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20)) { // Out of range; move there in formation this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 }); return ACCEPT_ORDER; } this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Heal": function(msg) { // TODO: on what should we base this range? if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) return this.FinishOrder(); if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return this.FinishOrder(); } this.CallMemberFunction("Heal", [msg.data.target, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.CollectTreasure": function(msg) { // TODO: on what should we base this range? if (this.CheckTargetRangeExplicit(msg.data.target, 0, 20)) { this.CallMemberFunction("CollectTreasure", [msg.data.target, false, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; } if (msg.data.secondTry || !this.CheckTargetVisible(msg.data.target)) return this.FinishOrder(); msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 20 }); return ACCEPT_ORDER; }, "Order.CollectTreasureNearPosition": function(msg) { // TODO: on what should we base this range? if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20)) { this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 }); return ACCEPT_ORDER; } this.CallMemberFunction("CollectTreasureNearPosition", [msg.data.x, msg.data.z, false, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Repair": function(msg) { // TODO: on what should we base this range? if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) return this.FinishOrder(); if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return this.FinishOrder(); } this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.ReturnResource": function(msg) { // TODO: on what should we base this range? if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.CheckTargetVisible(msg.data.target)) return this.FinishOrder(); if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return this.FinishOrder(); } this.CallMemberFunction("ReturnResource", [msg.data.target, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Pack": function(msg) { this.CallMemberFunction("Pack", [false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Unpack": function(msg) { this.CallMemberFunction("Unpack", [false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.DropAtNearestDropSite": function(msg) { this.CallMemberFunction("DropAtNearestDropSite", [false, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "IDLE": { "enter": function(msg) { // Turn rearrange off. Otherwise, if the formation is idle // but individual units go off to fight, // any death will rearrange the formation, which looks odd. // Instead, move idle units in formation on a timer. let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(false); // Start the timer on the next turn to catch up with potential stragglers. this.StartTimer(100, 2000); this.isIdle = true; this.CallMemberFunction("ResetIdle"); return false; }, "leave": function() { this.isIdle = false; this.StopTimer(); }, "Timer": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; if (this.TestAllMemberFunction("IsIdle")) cmpFormation.MoveMembersIntoFormation(false, false); }, }, "WALKING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopTimer(); this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.veryObstructed && !this.timer) { // It's possible that the controller (with large clearance) // is stuck, but not the individual units. // Ask them to move individually for a little while. this.CallMemberFunction("MoveTo", [this.order.data]); this.StartTimer(3000); return; } else if (this.timer) return; if (msg.likelyFailure || this.CheckRange(this.order.data)) this.FinishOrder(); }, "Timer": function() { // Reenter to reset the pathfinder state. this.SetNextState("WALKING"); } }, "WALKINGANDFIGHTING": { "enter": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true, "combat"); if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } this.StartTimer(0, 1000); this.order.data.returningState = "WALKINGANDFIGHTING"; return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { Engine.ProfileStart("FindWalkAndFightTargets"); if (this.FindWalkAndFightTargets()) this.SetNextState("MEMBER"); Engine.ProfileStop(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckRange(this.order.data)) this.FinishOrder(); }, }, "PATROL": { "enter": function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { this.FinishOrder(); return true; } // Memorize the origin position in case that we want to go back. if (!this.patrolStartPosOrder) { this.patrolStartPosOrder = cmpPosition.GetPosition(); this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; } this.SetAnimationVariant("combat"); return false; }, "leave": function() { delete this.patrolStartPosOrder; this.SetDefaultAnimationVariant(); }, "PATROLLING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true, "combat"); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld() || !this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } this.StartTimer(0, 1000); this.order.data.returningState = "PATROL.PATROLLING"; return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { if (this.FindWalkAndFightTargets()) this.SetNextState("MEMBER"); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange)) return; if (this.orderQueue.length == 1) this.PushOrder("Patrol", this.patrolStartPosOrder); this.PushOrder(this.order.type, this.order.data); this.SetNextState("CHECKINGWAYPOINT"); }, }, "CHECKINGWAYPOINT": { "enter": function() { this.StartTimer(0, 1000); this.stopSurveying = 0; // TODO: pick a proper animation return false; }, "leave": function() { this.StopTimer(); delete this.stopSurveying; }, "Timer": function(msg) { if (this.stopSurveying >= +this.template.PatrolWaitTime) { this.FinishOrder(); return; } if (this.FindWalkAndFightTargets()) this.SetNextState("MEMBER"); else ++this.stopSurveying; } } }, "GARRISON": { "APPROACHING": { "enter": function() { if (!this.MoveToTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable)) { this.FinishOrder(); return true; } let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); // If the holder should pickup, warn it so it can take needed action. let cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder); if (cmpHolder && cmpHolder.CanPickup(this.entity)) { this.pickup = this.order.data.target; // temporary, deleted in "leave" Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity, "iid": this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder }); } return false; }, "leave": function() { this.StopMoving(); if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.likelySuccess) this.SetNextState("GARRISONING"); }, }, "GARRISONING": { "enter": function() { this.CallMemberFunction(this.order.data.garrison ? "Garrison" : "OccupyTurret", [this.order.data.target, false]); // We might have been disbanded due to the lack of members. if (Engine.QueryInterface(this.entity, IID_Formation).GetMemberCount()) this.SetNextState("MEMBER"); return true; }, }, }, "FORMING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !this.CheckRange(this.order.data)) return; this.FinishOrder(); } }, "COMBAT": { "APPROACHING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true, "combat"); if (!this.MoveFormationToTargetAttackRange(this.order.data.target)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { let target = this.order.data.target; let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) target = cmpTargetUnitAI.GetFormationController(); let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.CallMemberFunction("Attack", [target, this.order.data.allowCapture, false]); if (cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else this.SetNextState("MEMBER"); }, }, "ATTACKING": { // Wait for individual members to finish "enter": function(msg) { const target = this.order.data.target; if (!this.CheckFormationTargetAttackRange(target)) { if (this.CanAttack(target) && this.CheckTargetVisible(target)) { this.SetNextState("COMBAT.APPROACHING"); return true; } this.FinishOrder(); return true; } let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // TODO fix the rearranging while attacking as formation cmpFormation.SetRearrange(!this.IsAttackingAsFormation()); cmpFormation.MoveMembersIntoFormation(false, false, "combat"); this.StartTimer(200, 200); return false; }, "Timer": function(msg) { const target = this.order.data.target; if (!this.CheckFormationTargetAttackRange(target)) { if (this.CanAttack(target) && this.CheckTargetVisible(target)) { this.SetNextState("COMBAT.APPROACHING"); return; } this.FinishOrder(); return; } }, "leave": function(msg) { this.StopTimer(); var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.SetRearrange(true); }, }, }, // Wait for individual members to finish "MEMBER": { "OrderTargetRenamed": function(msg) { // In general, don't react - we don't want to send spurious messages to members. // This looks odd for hunting however because we wait for all // entities to have clumped around the dead resource before proceeding // so explicitly handle this case. if (this.order && this.order.data && this.order.data.hunting && this.order.data.target == msg.data.newentity && this.orderQueue.length > 1) this.FinishOrder(); }, "enter": function(msg) { // Don't rearrange the formation, as that forces all units to stop // what they're doing. let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.SetRearrange(false); // While waiting on members, the formation is more like // a group of unit and does not have a well-defined position, // so move the controller out of the world to enforce that. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) cmpPosition.MoveOutOfWorld(); this.StartTimer(1000, 1000); return false; }, "Timer": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation && !cmpFormation.AreAllMembersFinished()) return; if (this.order?.data?.returningState) this.SetNextState(this.order.data.returningState); else this.FinishOrder(); }, "leave": function(msg) { this.StopTimer(); // Reform entirely as members might be all over the place now. let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation && (cmpFormation.AreAllMembersIdle() || this.orderQueue.length)) cmpFormation.MoveMembersIntoFormation(true); // Update the held position so entities respond to orders. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { let pos = cmpPosition.GetPosition2D(); this.CallMemberFunction("SetHeldPosition", [pos.x, pos.y]); } }, }, }, // States for entities moving as part of a formation: "FORMATIONMEMBER": { "FormationLeave": function(msg) { // Stop moving as soon as the formation disbands // Keep current rotation let facePointAfterMove = this.GetFacePointAfterMove(); this.SetFacePointAfterMove(false); this.StopMoving(); this.SetFacePointAfterMove(facePointAfterMove); // If the controller handled an order but some members rejected it, // they will have no orders and be in the FORMATIONMEMBER.IDLE state. if (this.orderQueue.length) { // We're leaving the formation, so stop our FormationWalk order if (this.FinishOrder()) return; } this.formationAnimationVariant = undefined; this.SetNextState("INDIVIDUAL.IDLE"); }, // Override the LeaveFoundation order since we're not doing // anything more important (and we might be stuck in the WALKING // state forever and need to get out of foundations in that case) "Order.LeaveFoundation": function(msg) { if (!this.WillMoveFromFoundation(msg.data.target)) return this.FinishOrder(); msg.data.min = g_LeaveFoundationRange; this.SetNextState("WALKINGTOPOINT"); return ACCEPT_ORDER; }, "enter": function() { let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { this.formationAnimationVariant = cmpFormation.GetFormationAnimationVariant(this.entity); if (this.formationAnimationVariant) this.SetAnimationVariant(this.formationAnimationVariant); else this.SetDefaultAnimationVariant(); } return false; }, "leave": function() { this.SetDefaultAnimationVariant(); this.formationAnimationVariant = undefined; }, "IDLE": "INDIVIDUAL.IDLE", "CHEERING": "INDIVIDUAL.CHEERING", "WALKING": { "enter": function() { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.MoveToFormationOffset(this.order.data.target, this.order.data.x, this.order.data.z); if (this.order.data.offsetsChanged) { let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) this.formationAnimationVariant = cmpFormation.GetFormationAnimationVariant(this.entity); } if (this.formationAnimationVariant) this.SetAnimationVariant(this.formationAnimationVariant); else if (this.order.data.variant) this.SetAnimationVariant(this.order.data.variant); else this.SetDefaultAnimationVariant(); return false; }, "leave": function() { // Don't use the logic from unitMotion, as SetInPosition // has already given us a custom rotation // (or we failed to move and thus don't care.) let facePointAfterMove = this.GetFacePointAfterMove(); this.SetFacePointAfterMove(false); this.StopMoving(); this.SetFacePointAfterMove(facePointAfterMove); }, // Occurs when the unit has reached its destination and the controller // is done moving. The controller is notified. "MovementUpdate": function(msg) { // When walking in formation, we'll only get notified in case of failure // if the formation controller has stopped walking. // Formations can start lagging a lot if many entities request short path // so prefer to finish order early than retry pathing. // (see https://code.wildfiregames.com/rP23806) // (if the message is likelyFailure of likelySuccess, we also want to stop). this.FinishOrder(); }, }, // Special case used by Order.LeaveFoundation "WALKINGTOPOINT": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function() { if (!this.CheckRange(this.order.data)) return; this.FinishOrder(); }, }, }, // States for entities not part of a formation: "INDIVIDUAL": { "Attacked": function(msg) { if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force) this.RespondToTargetedEntities([msg.data.attacker]); }, "GuardedAttacked": function(msg) { // do nothing if we have a forced order in queue before the guard order for (var i = 0; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].type == "Guard") break; if (this.orderQueue[i].data && this.orderQueue[i].data.force) return; } // if we already are targeting another unit still alive, finish with it first if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack")) if (this.order.data.target != msg.data.attacker && this.CanAttack(msg.data.attacker)) return; var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health); if (cmpIdentity && cmpIdentity.HasClass("Support") && cmpHealth && cmpHealth.IsInjured()) { if (this.CanHeal(this.isGuardOf)) this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false }); else if (this.CanRepair(this.isGuardOf)) this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); return; } var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI); if (cmpBuildingAI && this.CanRepair(this.isGuardOf)) { this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); return; } if (this.CheckTargetVisible(msg.data.attacker)) this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false }); else { var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false }); // if we already had a WalkAndFight, keep only the most recent one in case the target has moved if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight") { this.orderQueue.splice(1, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); } } }, "IDLE": { "Order.Cheer": function() { // Do not cheer if there is no cheering time and we are not idle yet. if (!this.cheeringTime || !this.isIdle) return this.FinishOrder(); this.SetNextState("CHEERING"); return ACCEPT_ORDER; }, "enter": function() { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation this.SelectAnimation("idle"); // Idle is the default state. If units try, from the IDLE.enter sub-state, to // begin another order, and that order fails (calling FinishOrder), they might // end up in an infinite loop. To avoid this, all methods that could put the unit in // a new state are done on the next turn. // This wastes a turn but avoids infinite loops. // Further, the GUI and AI want to know when a unit is idle, // but sending this info in Idle.enter will send spurious messages. // Pick 100 to execute on the next turn in SP and MP. this.StartTimer(100); return false; }, "leave": function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) cmpRangeManager.DisableActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) cmpRangeManager.DisableActiveQuery(this.losHealRangeQuery); if (this.losAttackRangeQuery) cmpRangeManager.DisableActiveQuery(this.losAttackRangeQuery); this.StopTimer(); if (this.isIdle) { if (this.IsFormationMember()) Engine.QueryInterface(this.formationController, IID_Formation).UnsetIdleEntity(this.entity); this.isIdle = false; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, "Attacked": function(msg) { if (this.isIdle && (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)) this.RespondToTargetedEntities([msg.data.attacker]); }, // On the range updates: // We check for idleness to prevent an entity to react only to newly seen entities // when receiving a Los*RangeUpdate on the same turn as the entity becomes idle // since this.FindNew*Targets is called in the timer. "LosRangeUpdate": function(msg) { if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length) this.RespondToSightedEntities(msg.data.added); }, "LosHealRangeUpdate": function(msg) { if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length) this.RespondToHealableEntities(msg.data.added); }, "LosAttackRangeUpdate": function(msg) { if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length && this.GetStance().targetVisibleEnemies) this.AttackEntitiesByPreference(msg.data.added); }, "Timer": function(msg) { if (this.isGuardOf) { this.Guard(this.isGuardOf, false); return; } // If a unit can heal and attack we first want to heal wounded units, // so check if we are a healer and find whether there's anybody nearby to heal. // (If anyone approaches later it'll be handled via LosHealRangeUpdate.) // If anyone in sight gets hurt that will be handled via LosHealRangeUpdate. if (this.IsHealer() && this.FindNewHealTargets()) return; // If we entered the idle state we must have nothing better to do, // so immediately check whether there's anybody nearby to attack. // (If anyone approaches later, it'll be handled via LosAttackRangeUpdate.) if (this.FindNewTargets()) return; if (this.FindSightedEnemies()) return; if (!this.isIdle) { // Move back to the held position if we drifted away. // (only if not a formation member). if (!this.IsFormationMember() && this.GetStance().respondHoldGround && this.heldPosition && !this.CheckPointRangeExplicit(this.heldPosition.x, this.heldPosition.z, 0, 10) && this.WalkToHeldPosition()) return; if (this.IsFormationMember()) { let cmpFormationAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (!cmpFormationAI || !cmpFormationAI.IsIdle()) return; Engine.QueryInterface(this.formationController, IID_Formation).SetIdleEntity(this.entity); } this.isIdle = true; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } // Go linger first to prevent all roaming entities // to move all at the same time on map init. if (this.template.RoamDistance) this.SetNextState("LINGERING"); }, "ROAMING": { "enter": function() { this.SetFacePointAfterMove(false); this.MoveRandomly(+this.template.RoamDistance); this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); this.SetFacePointAfterMove(true); }, "Timer": function(msg) { this.SetNextState("LINGERING"); }, "MovementUpdate": function() { this.MoveRandomly(+this.template.RoamDistance); }, }, "LINGERING": { "enter": function() { // ToDo: rename animations? this.SelectAnimation("feeding"); this.StartTimer(randIntInclusive(+this.template.FeedTimeMin, +this.template.FeedTimeMax)); return false; }, "leave": function() { this.ResetAnimation(); this.StopTimer(); }, "Timer": function(msg) { this.SetNextState("ROAMING"); }, }, }, "WALKING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { // If it looks like the path is failing, and we are close enough stop anyways. // This avoids pathing for an unreachable goal and reduces lag considerably. if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || this.CheckRange(this.order.data)) this.FinishOrder(); }, }, "WALKINGANDFIGHTING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); return false; }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "leave": function(msg) { this.StopMoving(); this.StopTimer(); this.SetDefaultAnimationVariant(); }, "MovementUpdate": function(msg) { // If it looks like the path is failing, and we are close enough stop anyways. // This avoids pathing for an unreachable goal and reduces lag considerably. if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || this.CheckRange(this.order.data)) this.FinishOrder(); }, }, "PATROL": { "enter": function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { this.FinishOrder(); return true; } // Memorize the origin position in case that we want to go back. if (!this.patrolStartPosOrder) { this.patrolStartPosOrder = cmpPosition.GetPosition(); this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; } this.SetAnimationVariant("combat"); return false; }, "leave": function() { delete this.patrolStartPosOrder; this.SetDefaultAnimationVariant(); }, "PATROLLING": { "enter": function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld() || !this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } this.StartTimer(0, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange)) return; if (this.orderQueue.length == 1) this.PushOrder("Patrol", this.patrolStartPosOrder); this.PushOrder(this.order.type, this.order.data); this.SetNextState("CHECKINGWAYPOINT"); }, }, "CHECKINGWAYPOINT": { "enter": function() { this.StartTimer(0, 1000); this.stopSurveying = 0; // TODO: pick a proper animation return false; }, "leave": function() { this.StopTimer(); delete this.stopSurveying; }, "Timer": function(msg) { if (this.stopSurveying >= +this.template.PatrolWaitTime) { this.FinishOrder(); return; } if (!this.FindWalkAndFightTargets()) ++this.stopSurveying; } } }, "GUARD": { "RemoveGuard": function() { this.FinishOrder(); }, "ESCORTING": { "enter": function() { if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) { this.FinishOrder(); return true; } // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); this.SetHeldPositionOnEntity(this.isGuardOf); return false; }, "Timer": function(msg) { if (!this.ShouldGuard(this.isGuardOf)) { this.FinishOrder(); return; } let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3 * this.guardRange, false)) this.TryMatchTargetSpeed(this.isGuardOf, false); this.SetHeldPositionOnEntity(this.isGuardOf); }, "leave": function(msg) { this.StopMoving(); this.StopTimer(); this.SetDefaultAnimationVariant(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("GUARDING"); }, }, "GUARDING": { "enter": function() { this.StartTimer(1000, 1000); this.SetHeldPositionOnEntity(this.entity); this.SetAnimationVariant("combat"); this.FaceTowardsTarget(this.order.data.target); return false; }, "LosAttackRangeUpdate": function(msg) { if (this.GetStance().targetVisibleEnemies) this.AttackEntitiesByPreference(msg.data.added); }, "Timer": function(msg) { if (!this.ShouldGuard(this.isGuardOf)) { this.FinishOrder(); return; } // TODO: find out what to do if we cannot move. if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange) && this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("ESCORTING"); else { this.FaceTowardsTarget(this.order.data.target); var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health); if (cmpHealth && cmpHealth.IsInjured()) { if (this.CanHeal(this.isGuardOf)) this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false }); else if (this.CanRepair(this.isGuardOf)) this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); } } }, "leave": function(msg) { this.StopTimer(); this.SetDefaultAnimationVariant(); }, }, }, "FLEEING": { "enter": function() { // We use the distance between the entities to account for ranged attacks this.order.data.distanceToFlee = PositionHelper.DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); // Use unit motion directly to ignore the visibility check. TODO: change this if we add LOS to fauna. if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) || !cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1)) { this.FinishOrder(); return true; } this.PlaySound("panic"); this.Run(); return false; }, "OrderTargetRenamed": function(msg) { // To avoid replaying the panic sound, handle this explicitly. let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) || !cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1)) this.FinishOrder(); }, "Attacked": function(msg) { if (msg.data.attacker == this.order.data.target) return; let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); if (cmpObstructionManager.DistanceToTarget(this.entity, msg.data.target) > cmpObstructionManager.DistanceToTarget(this.entity, this.order.data.target)) return; if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force) this.RespondToTargetedEntities([msg.data.attacker]); }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1)) this.FinishOrder(); }, }, "COMBAT": { "Order.LeaveFoundation": function(msg) { // Ignore the order as we're busy. return this.FinishOrder(); }, "Attacked": function(msg) { // If we're already in combat mode, ignore anyone else who's attacking us // unless it's a melee attack since they may be blocking our way to the target if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force)) this.RespondToTargetedEntities([msg.data.attacker]); }, "leave": function() { if (!this.formationAnimationVariant) this.SetDefaultAnimationVariant(); }, "APPROACHING": { "enter": function() { if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) { this.FinishOrder(); return true; } if (!this.formationAnimationVariant) this.SetAnimationVariant("combat"); this.StartTimer(1000, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType)) { this.FinishOrder(); if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } else { this.RememberTargetPosition(); if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); } }, "MovementUpdate": function(msg) { if (msg.likelyFailure) { // This also handles hunting. if (this.orderQueue.length > 1) { this.FinishOrder(); return; } else if (!this.order.data.force || !this.order.data.lastPos) { this.SetNextState("COMBAT.FINDINGNEWTARGET"); return; } // If the order was forced, try moving to the target position, // under the assumption that this is desirable if the target // was somewhat far away - we'll likely end up closer to where // the player hoped we would. let lastPos = this.order.data.lastPos; this.PushOrder("WalkAndFight", { "x": lastPos.x, "z": lastPos.z, "force": false, }); return; } if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { if (this.CanUnpack()) { this.PushOrderFront("Unpack", { "force": true }); return; } this.SetNextState("ATTACKING"); } else if (msg.likelySuccess) // Try moving again, // attack range uses a height-related formula and our actual max range might have changed. if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) this.FinishOrder(); }, }, "ATTACKING": { "enter": function() { let target = this.order.data.target; let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) { this.order.data.formationTarget = target; target = cmpFormation.GetClosestMember(this.entity); this.order.data.target = target; } this.shouldCheer = false; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) { this.FinishOrder(); return true; } if (!this.CheckTargetAttackRange(target, this.order.data.attackType)) { if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return true; } this.ProcessMessage("OutOfRange"); return true; } if (!this.formationAnimationVariant) this.SetAnimationVariant("combat"); this.FaceTowardsTarget(this.order.data.target); this.RememberTargetPosition(); if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); if (!cmpAttack.StartAttacking(this.order.data.target, this.order.data.attackType, IID_UnitAI)) { this.ProcessMessage("TargetInvalidated"); return true; } let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) { cmpBuildingAI.SetUnitAITarget(this.order.data.target); return false; } let cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); // Units with no cheering time do not cheer. this.shouldCheer = cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()) && this.cheeringTime > 0; return false; }, "leave": function() { let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (cmpAttack) cmpAttack.StopAttacking(); }, "OutOfRange": function() { if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force)) { if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return; } this.SetNextState("CHASING"); return; } this.SetNextState("FINDINGNEWTARGET"); }, "TargetInvalidated": function() { this.SetNextState("FINDINGNEWTARGET"); }, "Attacked": function(msg) { if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) && this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture") this.RespondToTargetedEntities([msg.data.attacker]); }, }, "FINDINGNEWTARGET": { "Order.Cheer": function() { if (!this.cheeringTime) return this.FinishOrder(); this.SetNextState("CHEERING"); return ACCEPT_ORDER; }, "enter": function() { // Try to find the formation the target was a part of. let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation); if (!cmpFormation) cmpFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation); // If the target is a formation, pick closest member. if (cmpFormation) { let filter = (t) => this.CanAttack(t); this.order.data.formationTarget = this.order.data.target; let target = cmpFormation.GetClosestMember(this.entity, filter); this.order.data.target = target; this.SetNextState("COMBAT.ATTACKING"); return true; } // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up // except if in WalkAndFight mode where we look for more enemies around before moving again. if (this.FinishOrder()) { if (this.IsWalkingAndFighting()) { Engine.ProfileStart("FindWalkAndFightTargets"); this.FindWalkAndFightTargets(); Engine.ProfileStop(); } return true; } if (this.FindNewTargets()) return true; if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); if (this.shouldCheer) { this.Cheer(); this.CallPlayerOwnedEntitiesFunctionInRange("Cheer", [], this.notifyToCheerInRange); } return true; }, }, "CHASING": { "Order.MoveToChasingPoint": function(msg) { if (this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, msg.data.max) || !this.AbleToMove()) return this.FinishOrder(); msg.data.relaxed = true; this.StopTimer(); this.SetNextState("MOVINGTOPOINT"); return ACCEPT_ORDER; }, "enter": function() { if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) { this.FinishOrder(); return true; } if (!this.formationAnimationVariant) this.SetAnimationVariant("combat"); if (Engine.QueryInterface(this.order.data.target, IID_UnitAI)?.IsFleeing()) this.Run(); this.StartTimer(1000, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType)) { this.FinishOrder(); if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } else { this.RememberTargetPosition(); if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); } }, "MovementUpdate": function(msg) { if (msg.likelyFailure) { // This also handles hunting. if (this.orderQueue.length > 1) { this.FinishOrder(); return; } else if (!this.order.data.force) { this.SetNextState("COMBAT.FINDINGNEWTARGET"); return; } else if (this.order.data.lastPos) { let lastPos = this.order.data.lastPos; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.PushOrder("MoveToChasingPoint", { "x": lastPos.x, "z": lastPos.z, "max": cmpAttack.GetRange(this.order.data.attackType).max, "force": true }); return; } } if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { if (this.CanUnpack()) { this.PushOrderFront("Unpack", { "force": true }); return; } this.SetNextState("ATTACKING"); } else if (msg.likelySuccess) // Try moving again, // attack range uses a height-related formula and our actual max range might have changed. if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) this.FinishOrder(); }, "MOVINGTOPOINT": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { // If it looks like the path is failing, and we are close enough from wanted range // stop anyways. This avoids pathing for an unreachable goal and reduces lag considerably. if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.order.data.max + this.DefaultRelaxedMaxRange) || !msg.obstructed && this.CheckRange(this.order.data)) this.FinishOrder(); }, }, }, }, "GATHER": { "enter": function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) cmpResourceGatherer.AddToPlayerCounter(this.order.data.type.generic); return false; }, "leave": function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) cmpResourceGatherer.RemoveFromPlayerCounter(); // Show the carried resource, if we've gathered anything. this.SetDefaultAnimationVariant(); }, "APPROACHING": { "enter": function() { this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". if (this.CheckRange(this.order.data, IID_ResourceGatherer)) { this.SetNextState("GATHERING"); return true; } // If we can't move, assume we'll fail any subsequent order // and finish the order entirely to avoid an infinite loop. if (!this.AbleToMove()) { this.FinishOrder(); return true; } let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); let cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage); if ((!cmpMirage || !cmpMirage.Mirages(IID_ResourceSupply)) && (!cmpSupply || !cmpSupply.AddGatherer(this.entity)) || !this.MoveTo(this.order.data, IID_ResourceGatherer)) { // If the target's last known position is in FOW, try going there // and hope that we might find it then. let lastPos = this.order.data.lastPos; if (this.gatheringTarget != INVALID_ENTITY && lastPos && !this.CheckPositionVisible(lastPos.x, lastPos.z)) { this.PushOrderFront("Walk", { "x": lastPos.x, "z": lastPos.z, "force": this.order.data.force }); return true; } this.SetNextState("FINDINGNEWTARGET"); return true; } this.SetAnimationVariant("approach_" + this.order.data.type.specific); return false; }, "MovementUpdate": function(msg) { // The GATHERING timer will handle finding a valid resource. if (msg.likelyFailure) this.SetNextState("FINDINGNEWTARGET"); else if (this.CheckRange(this.order.data, IID_ResourceGatherer)) this.SetNextState("GATHERING"); }, "leave": function() { this.StopMoving(); this.SetDefaultAnimationVariant(); if (!this.gatheringTarget) return; let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; }, }, // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } this.SetAnimationVariant("approach_" + this.order.data.type.specific); return false; }, "leave": function() { this.StopMoving(); this.SetDefaultAnimationVariant(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || this.CheckRange(this.order.data)) this.SetNextState("FINDINGNEWTARGET"); }, }, "GATHERING": { "enter": function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) { this.FinishOrder(); return true; } if (!this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) { this.ProcessMessage("OutOfRange"); return true; } // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) this.order.data.force = false; this.order.data.autoharvest = true; this.FaceTowardsTarget(this.order.data.target); if (!cmpResourceGatherer.StartGathering(this.order.data.target, IID_UnitAI)) { this.ProcessMessage("TargetInvalidated"); return true; } return false; }, "leave": function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) cmpResourceGatherer.StopGathering(); }, "InventoryFilled": function(msg) { this.SetNextState("RETURNINGRESOURCE"); }, "OutOfRange": function(msg) { if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) this.SetNextState("APPROACHING"); // Our target is no longer visible - go to its last known position first // and then hopefully it will become visible. else if (!this.CheckTargetVisible(this.order.data.target) && this.order.data.lastPos) this.PushOrderFront("Walk", { "x": this.order.data.lastPos.x, "z": this.order.data.lastPos.z, "force": this.order.data.force }); else this.SetNextState("FINDINGNEWTARGET"); }, "TargetInvalidated": function(msg) { this.SetNextState("FINDINGNEWTARGET"); }, }, "FINDINGNEWTARGET": { "enter": function() { const previousForced = this.order.data.force; let previousTarget = this.order.data.target; let resourceTemplate = this.order.data.template; let resourceType = this.order.data.type; // Give up on this order and try our next queued order // but first check what is our next order and, if needed, insert a returnResource order let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer.IsCarrying(resourceType.generic) && this.orderQueue.length > 1 && this.orderQueue[1] !== "ReturnResource" && (this.orderQueue[1].type !== "Gather" || this.orderQueue[1].data.type.generic !== resourceType.generic)) { let nearestDropsite = this.FindNearestDropsite(resourceType.generic); if (nearestDropsite) this.orderQueue.splice(1, 0, { "type": "ReturnResource", "data": { "target": nearestDropsite, "force": false } }); } // Must go before FinishOrder or this.order will be undefined. let initPos = this.order.data.initPos; if (this.FinishOrder()) return true; // No remaining orders - pick a useful default behaviour let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return true; let filter = (ent, type, template) => { if (previousTarget == ent) return false; // Don't switch to a different type of huntable animal. return type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template); }; // Current position is often next to a dropsite. // But don't use that on forced orders, as the order may want us to go // to the other side of the map on purpose. let pos = cmpPosition.GetPosition(); let nearbyResource; if (!previousForced) nearbyResource = this.FindNearbyResource(Vector2D.from3D(pos), filter); // If there is an initPos, search there as well when we haven't found anything. // Otherwise set initPos to our current pos. if (!initPos) initPos = { 'x': pos.X, 'z': pos.Z }; else if (!nearbyResource || previousForced) nearbyResource = this.FindNearbyResource(new Vector2D(initPos.x, initPos.z), filter); if (nearbyResource) { this.PerformGather(nearbyResource, false, false); return true; } // Failing that, try to move there and se if we are more lucky: maybe there are resources in FOW. // Only move if we are some distance away (TODO: pick the distance better?). // Using the default relaxed range check since that is used in the WALKING-state. if (!this.CheckPointRangeExplicit(initPos.x, initPos.z, 0, this.DefaultRelaxedMaxRange)) { this.GatherNearPosition(initPos.x, initPos.z, resourceType, resourceTemplate); return true; } // Nothing else to gather - if we're carrying anything then we should // drop it off, and if not then we might as well head to the dropsite // anyway because that's a nice enough place to congregate and idle let nearestDropsite = this.FindNearestDropsite(resourceType.generic); if (nearestDropsite) { this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false }); return true; } // No dropsites - just give up. return true; }, }, "RETURNINGRESOURCE": { "enter": function() { let nearestDropsite = this.FindNearestDropsite(this.order.data.type.generic); if (!nearestDropsite) { // The player expects the unit to move upon failure. let formerTarget = this.order.data.target; if (!this.FinishOrder()) this.WalkToTarget(formerTarget); return true; } this.order.data.formerTarget = this.order.data.target; this.order.data.target = nearestDropsite; if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) { this.SetNextState("DROPPINGRESOURCES"); return true; } this.SetNextState("APPROACHING"); return true; }, "leave": function() { }, "APPROACHING": "INDIVIDUAL.RETURNRESOURCE.APPROACHING", "DROPPINGRESOURCES": { "enter": function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer) && cmpResourceGatherer.IsTargetInRange(this.order.data.target)) { cmpResourceGatherer.CommitResources(this.order.data.target); // Stop showing the carried resource animation. this.SetDefaultAnimationVariant(); this.SetNextState("GATHER.APPROACHING"); } else this.SetNextState("RETURNINGRESOURCE"); this.order.data.target = this.order.data.formerTarget; return true; }, "leave": function() { }, }, }, }, "HEAL": { "APPROACHING": { "enter": function() { if (this.CheckRange(this.order.data, IID_Heal)) { this.SetNextState("HEALING"); return true; } if (!this.MoveTo(this.order.data, IID_Heal)) { this.FinishOrder(); return true; } this.StartTimer(1000, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null)) this.SetNextState("FINDINGNEWTARGET"); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckRange(this.order.data, IID_Heal)) this.SetNextState("HEALING"); }, }, "HEALING": { "enter": function() { let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); if (!cmpHeal) { this.FinishOrder(); return true; } if (!this.CheckRange(this.order.data, IID_Heal)) { this.ProcessMessage("OutOfRange"); return true; } if (!cmpHeal.StartHealing(this.order.data.target, IID_UnitAI)) { this.ProcessMessage("TargetInvalidated"); return true; } this.FaceTowardsTarget(this.order.data.target); return false; }, "leave": function() { let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); if (cmpHeal) cmpHeal.StopHealing(); }, "OutOfRange": function(msg) { if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force)) { if (this.CanPack()) this.PushOrderFront("Pack", { "force": true }); else this.SetNextState("APPROACHING"); } else this.SetNextState("FINDINGNEWTARGET"); }, "TargetInvalidated": function(msg) { this.SetNextState("FINDINGNEWTARGET"); }, }, "FINDINGNEWTARGET": { "enter": function() { // If we have another order, do that instead. if (this.FinishOrder()) return true; if (this.FindNewHealTargets()) return true; if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); // We quit this state right away. return true; }, }, }, // Returning to dropsite "RETURNRESOURCE": { "APPROACHING": { "enter": function() { if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) { this.SetNextState("DROPPINGRESOURCES"); return true; } if (!this.MoveTo(this.order.data, IID_ResourceGatherer)) { this.FinishOrder(); return true; } this.SetDefaultAnimationVariant(); return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) this.SetNextState("DROPPINGRESOURCES"); }, }, "DROPPINGRESOURCES": { "enter": function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer) && cmpResourceGatherer.IsTargetInRange(this.order.data.target)) { cmpResourceGatherer.CommitResources(this.order.data.target); // Stop showing the carried resource animation. this.SetDefaultAnimationVariant(); this.FinishOrder(); return true; } let nearby = this.FindNearestDropsite(cmpResourceGatherer.GetMainCarryingType()); this.FinishOrder(); if (nearby) this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return true; }, "leave": function() { }, }, }, "COLLECTTREASURE": { "leave": function() { }, "APPROACHING": { "enter": function() { // If we can't move, assume we'll fail any subsequent order // and finish the order entirely to avoid an infinite loop. if (!this.AbleToMove()) { this.FinishOrder(); return true; } if (!this.MoveToTargetRange(this.order.data.target, IID_TreasureCollector)) { this.SetNextState("FINDINGNEWTARGET"); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollector)) this.SetNextState("COLLECTING"); else if (msg.likelyFailure) this.SetNextState("FINDINGNEWTARGET"); }, }, "COLLECTING": { "enter": function() { let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector); if (!cmpTreasureCollector.StartCollecting(this.order.data.target, IID_UnitAI)) { this.ProcessMessage("TargetInvalidated"); return true; } this.FaceTowardsTarget(this.order.data.target); return false; }, "leave": function() { let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector); if (cmpTreasureCollector) cmpTreasureCollector.StopCollecting(); }, "OutOfRange": function(msg) { this.SetNextState("APPROACHING"); }, "TargetInvalidated": function(msg) { this.SetNextState("FINDINGNEWTARGET"); }, }, "FINDINGNEWTARGET": { "enter": function() { let oldTarget = this.order.data.target || INVALID_ENTITY; // Switch to the next order (if any). if (this.FinishOrder()) return true; let nearbyTreasure = this.FindNearbyTreasure(this.TargetPosOrEntPos(oldTarget)); if (nearbyTreasure) this.CollectTreasure(nearbyTreasure, true); return true; }, }, // Walking to a good place to collect treasures near, used by CollectTreasureNearPosition. "WALKING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || this.CheckRange(this.order.data)) this.SetNextState("FINDINGNEWTARGET"); }, }, }, "TRADE": { "Attacked": function(msg) { // Ignore attack // TODO: Inform player }, "leave": function() { }, "APPROACHINGMARKET": { "enter": function() { if (!this.MoveToMarket(this.order.data.target)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !this.CheckRange(this.order.data.nextTarget, IID_Trader)) return; if (this.waypoints && this.waypoints.length) { if (!this.MoveToMarket(this.order.data.target)) this.FinishOrder(); } else this.SetNextState("TRADING"); }, }, "TRADING": { "enter": function() { if (!this.CanTrade(this.order.data.target)) { this.FinishOrder(); return true; } if (!this.CheckTargetRange(this.order.data.target, IID_Trader)) { this.SetNextState("APPROACHINGMARKET"); return true; } let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); let nextMarket = cmpTrader.PerformTrade(this.order.data.target); let amount = cmpTrader.GetGoods().amount; if (!nextMarket || !amount || !amount.traderGain) { this.FinishOrder(); return true; } this.order.data.target = nextMarket; if (this.order.data.route && this.order.data.route.length) { this.waypoints = this.order.data.route.slice(); if (this.order.data.target == cmpTrader.GetSecondMarket()) this.waypoints.reverse(); } this.SetNextState("APPROACHINGMARKET"); return true; }, "leave": function() { }, }, "TradingCanceled": function(msg) { if (msg.market != this.order.data.target) return; let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); let otherMarket = cmpTrader && cmpTrader.GetFirstMarket(); if (otherMarket) this.WalkToTarget(otherMarket); else this.FinishOrder(); }, }, "REPAIR": { "APPROACHING": { "enter": function() { if (!this.MoveTo(this.order.data, IID_Builder)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.likelySuccess) this.SetNextState("REPAIRING"); }, }, "REPAIRING": { "enter": function() { let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); if (!cmpBuilder) { this.FinishOrder(); return true; } // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) if (this.order.data.force) this.order.data.autoharvest = true; this.order.data.force = false; if (!this.CheckTargetRange(this.order.data.target, IID_Builder)) { this.ProcessMessage("OutOfRange"); return true; } let cmpHealth = Engine.QueryInterface(this.order.data.target, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints()) { // The building was already finished/fully repaired before we arrived; // let the ConstructionFinished handler handle this. this.ConstructionFinished({ "entity": this.order.data.target, "newentity": this.order.data.target }); return true; } if (!cmpBuilder.StartRepairing(this.order.data.target, IID_UnitAI)) { this.ProcessMessage("TargetInvalidated"); return true; } this.FaceTowardsTarget(this.order.data.target); return false; }, "leave": function() { let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); if (cmpBuilder) cmpBuilder.StopRepairing(); }, "OutOfRange": function(msg) { this.SetNextState("APPROACHING"); }, "TargetInvalidated": function(msg) { this.FinishOrder(); }, }, "ConstructionFinished": function(msg) { if (msg.data.entity != this.order.data.target) return; // ignore other buildings let oldData = this.order.data; // Save the current state so we can continue walking if necessary // FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation. // Idle animation while moving towards finished construction looks weird (ghosty). let oldState = this.GetCurrentState(); let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); let canReturnResources = this.CanReturnResource(msg.data.newentity, true, cmpResourceGatherer); if (this.CheckTargetRange(msg.data.newentity, IID_Builder) && canReturnResources) { cmpResourceGatherer.CommitResources(msg.data.newentity); this.SetDefaultAnimationVariant(); } // Switch to the next order (if any) if (this.FinishOrder()) { if (canReturnResources) { // We aren't in range, but we can still return resources there: always do so. this.SetDefaultAnimationVariant(); this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false }); } return; } if (canReturnResources) { // We aren't in range, but we can still return resources there: always do so. this.SetDefaultAnimationVariant(); this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false }); } // No remaining orders - pick a useful default behaviour // If autocontinue explicitly disabled (e.g. by AI) then // do nothing automatically if (!oldData.autocontinue) return; // If this building was e.g. a farm of ours, the entities that received // the build command should start gathering from it if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity)) { this.PerformGather(msg.data.newentity, true, false); return; } // If this building was e.g. a farmstead of ours, entities that received // the build command should look for nearby resources to gather if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false, cmpResourceGatherer)) { let cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); let types = cmpResourceDropsite.GetTypes(); // TODO: Slightly undefined behavior here, we don't know what type of resource will be collected, // may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that! let nearby = this.FindNearbyResource(this.TargetPosOrEntPos(msg.data.newentity), (ent, type, template) => types.indexOf(type.generic) != -1); if (nearby) { this.PerformGather(nearby, true, false); return; } } let nearbyFoundation = this.FindNearbyFoundation(this.TargetPosOrEntPos(msg.data.newentity)); if (nearbyFoundation) { this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true); return; } // Unit was approaching and there's nothing to do now, so switch to walking if (oldState.endsWith("REPAIR.APPROACHING")) // We're already walking to the given point, so add this as a order. this.WalkToTarget(msg.data.newentity, true); }, }, "GARRISON": { "APPROACHING": { "enter": function() { if (this.order.data.garrison ? !this.CanGarrison(this.order.data.target) : !this.CanOccupyTurret(this.order.data.target)) { this.FinishOrder(); return true; } if (!this.MoveToTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable)) { this.FinishOrder(); return true; } if (this.pickup) Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); let cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder); if (cmpHolder && cmpHolder.CanPickup(this.entity)) { this.pickup = this.order.data.target; Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity, "iid": this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder }); } return false; }, "leave": function() { if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } this.StopMoving(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !msg.likelySuccess) return; if (this.CheckTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable)) this.SetNextState("GARRISONING"); else { // Unable to reach the target, try again (or follow if it is a moving target) // except if the target does not exist anymore or its orders have changed. if (this.pickup) { let cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI); if (!cmpUnitAI || (!cmpUnitAI.HasPickupOrder(this.entity) && !cmpUnitAI.IsIdle())) this.FinishOrder(); } } }, }, "GARRISONING": { "enter": function() { let target = this.order.data.target; if (this.order.data.garrison) { let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable); if (!cmpGarrisonable || !cmpGarrisonable.Garrison(target)) { this.FinishOrder(); return true; } } else { let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable); if (!cmpTurretable || !cmpTurretable.OccupyTurret(target)) { this.FinishOrder(); return true; } } if (this.formationController) { let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { let rearrange = cmpFormation.rearrange; cmpFormation.SetRearrange(false); cmpFormation.RemoveMembers([this.entity]); cmpFormation.SetRearrange(rearrange); } } let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (this.CanReturnResource(target, true, cmpResourceGatherer)) { cmpResourceGatherer.CommitResources(target); this.SetDefaultAnimationVariant(); } this.FinishOrder(); return true; }, "leave": function() { }, }, }, "CHEERING": { "enter": function() { this.SelectAnimation("promotion"); this.StartTimer(this.cheeringTime); return false; }, "leave": function() { // PushOrderFront preserves the cheering order, // which can lead to very bad behaviour, so make // sure to delete any queued ones. for (let i = 1; i < this.orderQueue.length; ++i) if (this.orderQueue[i].type == "Cheer") this.orderQueue.splice(i--, 1); this.StopTimer(); this.ResetAnimation(); }, "LosRangeUpdate": function(msg) { if (msg && msg.data && msg.data.added && msg.data.added.length) this.RespondToSightedEntities(msg.data.added); }, "LosHealRangeUpdate": function(msg) { if (msg && msg.data && msg.data.added && msg.data.added.length) this.RespondToHealableEntities(msg.data.added); }, "LosAttackRangeUpdate": function(msg) { if (msg && msg.data && msg.data.added && msg.data.added.length && this.GetStance().targetVisibleEnemies) this.AttackEntitiesByPreference(msg.data.added); }, "Timer": function(msg) { this.FinishOrder(); }, }, "PACKING": { "enter": function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Pack(); return false; }, "Order.CancelPack": function(msg) { this.FinishOrder(); return ACCEPT_ORDER; }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.CancelPack(); }, "Attacked": function(msg) { // Ignore attacks while packing }, }, "UNPACKING": { "enter": function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Unpack(); return false; }, "Order.CancelUnpack": function(msg) { this.FinishOrder(); return ACCEPT_ORDER; }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.CancelPack(); }, "Attacked": function(msg) { // Ignore attacks while unpacking }, }, "PICKUP": { "APPROACHING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.likelySuccess) this.SetNextState("LOADING"); }, "PickupCanceled": function() { this.FinishOrder(); }, }, "LOADING": { "enter": function() { let cmpHolder = Engine.QueryInterface(this.entity, this.order.data.iid); if (!cmpHolder || cmpHolder.IsFull()) { this.FinishOrder(); return true; } return false; }, "PickupCanceled": function() { this.FinishOrder(); }, }, }, }, }; UnitAI.prototype.Init = function() { this.orderQueue = []; // current order is at the front of the list this.order = undefined; // always == this.orderQueue[0] this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to this.isIdle = false; this.heldPosition = undefined; // Queue of remembered works this.workOrders = []; this.isGuardOf = undefined; this.formationAnimationVariant = undefined; this.cheeringTime = +(this.template.CheeringTime || 0); this.SetStance(this.template.DefaultStance); }; /** * @param {cmpTurretable} cmpTurretable - Optionally the component to save a query here. * @return {boolean} - Whether we are occupying a turret point. */ UnitAI.prototype.IsTurret = function(cmpTurretable) { if (!cmpTurretable) cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable); return cmpTurretable && cmpTurretable.HolderID() != INVALID_ENTITY; }; UnitAI.prototype.IsFormationController = function() { return (this.template.FormationController == "true"); }; UnitAI.prototype.IsFormationMember = function() { return (this.formationController != INVALID_ENTITY); }; UnitAI.prototype.GetFormationsList = function() { return this.template.Formations?._string?.split(/\s+/) || []; }; UnitAI.prototype.CanUseFormation = function(formation) { return this.GetFormationsList().includes(formation); }; /** * For now, entities with a RoamDistance are animals. */ UnitAI.prototype.IsAnimal = function() { return !!this.template.RoamDistance; }; /** * ToDo: Make this not needed by fixing gaia * range queries in BuildingAI and UnitAI regarding * animals and other gaia entities. */ UnitAI.prototype.IsDangerousAnimal = function() { return this.IsAnimal() && this.GetStance().targetVisibleEnemies && !!Engine.QueryInterface(this.entity, IID_Attack); }; UnitAI.prototype.IsHealer = function() { return Engine.QueryInterface(this.entity, IID_Heal); }; UnitAI.prototype.IsIdle = function() { return this.isIdle; }; /** * Used by formation controllers to toggle the idleness of their members. */ UnitAI.prototype.ResetIdle = function() { let shouldBeIdle = this.GetCurrentState().endsWith(".IDLE"); if (this.isIdle == shouldBeIdle) return; this.isIdle = shouldBeIdle; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); }; UnitAI.prototype.SetGarrisoned = function() { // UnitAI caches its own garrisoned state for performance. this.isGarrisoned = true; this.SetImmobile(); }; UnitAI.prototype.UnsetGarrisoned = function() { delete this.isGarrisoned; this.SetMobile(); }; UnitAI.prototype.ShouldRespondToEndOfAlert = function() { return !this.orderQueue.length || this.orderQueue[0].type == "Garrison"; }; UnitAI.prototype.SetImmobile = function() { if (this.isImmobile) return; this.isImmobile = true; Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, { "entity": this.entity, "ableToMove": this.AbleToMove() }); }; UnitAI.prototype.SetMobile = function() { if (!this.isImmobile) return; delete this.isImmobile; Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, { "entity": this.entity, "ableToMove": this.AbleToMove() }); }; /** * @param cmpUnitMotion - optionally pass unitMotion to avoid querying it here * @returns true if the entity can move, i.e. has UnitMotion and isn't immobile. */ UnitAI.prototype.AbleToMove = function(cmpUnitMotion) { if (this.isImmobile) return false; if (!cmpUnitMotion) cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return !!cmpUnitMotion; }; UnitAI.prototype.IsFleeing = function() { var state = this.GetCurrentState().split(".").pop(); return (state == "FLEEING"); }; UnitAI.prototype.IsWalking = function() { var state = this.GetCurrentState().split(".").pop(); return (state == "WALKING"); }; /** * Return true if the current order is WalkAndFight or Patrol. */ UnitAI.prototype.IsWalkingAndFighting = function() { if (this.IsFormationMember()) return Engine.QueryInterface(this.formationController, IID_UnitAI).IsWalkingAndFighting(); return this.orderQueue.length > 0 && (this.orderQueue[0].type == "WalkAndFight" || this.orderQueue[0].type == "Patrol"); }; UnitAI.prototype.OnCreate = function() { if (this.IsFormationController()) this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE"); else this.UnitFsm.Init(this, "INDIVIDUAL.IDLE"); this.isIdle = true; }; UnitAI.prototype.OnDiplomacyChanged = function(msg) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) this.SetupRangeQueries(); if (this.isGuardOf && !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf)) this.RemoveGuard(); }; UnitAI.prototype.OnOwnershipChanged = function(msg) { this.SetupRangeQueries(); if (this.isGuardOf && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf))) this.RemoveGuard(); // If the unit isn't being created or dying, reset stance and clear orders if (msg.to != INVALID_PLAYER && msg.from != INVALID_PLAYER) { // Switch to a virgin state to let states execute their leave handlers. // Except if (un)packing, in which case we only clear the order queue. if (this.IsPacking()) { this.orderQueue.length = Math.min(this.orderQueue.length, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); } else { const state = this.GetCurrentState(); // Special "will be destroyed soon" mode - do nothing. if (state === "") return; const index = state.indexOf("."); if (index != -1) this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0, index)); this.Stop(false); } this.workOrders = []; let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader) cmpTrader.StopTrading(); this.SetStance(this.template.DefaultStance); if (this.IsTurret()) this.SetTurretStance(); } }; UnitAI.prototype.OnDestroy = function() { // Switch to an empty state to let states execute their leave handlers. this.UnitFsm.SwitchToNextState(this, ""); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) cmpRangeManager.DestroyActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery); if (this.losAttackRangeQuery) cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery); }; UnitAI.prototype.OnVisionRangeChanged = function(msg) { if (this.entity == msg.entity) this.SetupRangeQueries(); }; UnitAI.prototype.HasPickupOrder = function(entity) { return this.orderQueue.some(order => order.type == "PickupUnit" && order.data.target == entity); }; UnitAI.prototype.OnPickupRequested = function(msg) { if (this.HasPickupOrder(msg.entity)) return; this.PushOrderAfterForced("PickupUnit", { "target": msg.entity, "iid": msg.iid }); }; UnitAI.prototype.OnPickupCanceled = function(msg) { for (let i = 0; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].type != "PickupUnit" || this.orderQueue[i].data.target != msg.entity) continue; if (i == 0) this.UnitFsm.ProcessMessage(this, { "type": "PickupCanceled", "data": msg }); else this.orderQueue.splice(i, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); break; } }; /** * Wrapper function that sets up the LOS, healer and attack range queries. * This should be called whenever our ownership changes. */ UnitAI.prototype.SetupRangeQueries = function() { if (this.GetStance().respondFleeOnSight) this.SetupLOSRangeQuery(); if (this.IsHealer()) this.SetupHealRangeQuery(); if (Engine.QueryInterface(this.entity, IID_Attack)) this.SetupAttackRangeQuery(); }; UnitAI.prototype.UpdateRangeQueries = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) this.SetupLOSRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losRangeQuery)); if (this.losHealRangeQuery) this.SetupHealRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losHealRangeQuery)); if (this.losAttackRangeQuery) this.SetupAttackRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losAttackRangeQuery)); }; /** * Set up a range query for all enemy units within LOS range. * @param {boolean} enable - Optional parameter whether to enable the query. */ UnitAI.prototype.SetupLOSRangeQuery = function(enable = true) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) { cmpRangeManager.DestroyActiveQuery(this.losRangeQuery); this.losRangeQuery = undefined; } - let cmpPlayer = QueryOwnerInterface(this.entity); - // If we are being destructed (owner == -1), creating a range query is pointless. - if (!cmpPlayer) + const cmpDiplomacy = QueryOwnerInterface(this.entity, IID_Diplomacy); + if (!cmpDiplomacy) return; - let players = cmpPlayer.GetEnemies(); + const players = cmpDiplomacy.GetEnemies(); if (!players.length) return; let range = this.GetQueryRange(IID_Vision); // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that. this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Identity, cmpRangeManager.GetEntityFlagMask("normal"), false); if (enable) cmpRangeManager.EnableActiveQuery(this.losRangeQuery); }; /** * Set up a range query for all own or ally units within LOS range * which can be healed. * @param {boolean} enable - Optional parameter whether to enable the query. */ UnitAI.prototype.SetupHealRangeQuery = function(enable = true) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losHealRangeQuery) { cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery); this.losHealRangeQuery = undefined; } - let cmpPlayer = QueryOwnerInterface(this.entity); - // If we are being destructed (owner == -1), creating a range query is pointless. - if (!cmpPlayer) + const cmpDiplomacy = QueryOwnerInterface(this.entity, IID_Diplomacy); + if (!cmpDiplomacy) return; - let players = cmpPlayer.GetAllies(); - let range = this.GetQueryRange(IID_Heal); + const players = cmpDiplomacy.GetAllies(); + const range = this.GetQueryRange(IID_Heal); // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that. this.losHealRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, cmpRangeManager.GetEntityFlagMask("injured"), false); if (enable) cmpRangeManager.EnableActiveQuery(this.losHealRangeQuery); }; /** * Set up a range query for all enemy and gaia units within range * which can be attacked. * @param {boolean} enable - Optional parameter whether to enable the query. */ UnitAI.prototype.SetupAttackRangeQuery = function(enable = true) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losAttackRangeQuery) { cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery); this.losAttackRangeQuery = undefined; } - let cmpPlayer = QueryOwnerInterface(this.entity); - // If we are being destructed (owner == -1), creating a range query is pointless. - if (!cmpPlayer) + const cmpDiplomacy = QueryOwnerInterface(this.entity, IID_Diplomacy); + if (!cmpDiplomacy) return; // TODO: How to handle neutral players - Special query to attack military only? - let players = cmpPlayer.GetEnemies(); + const players = cmpDiplomacy.GetEnemies(); if (!players.length) return; let range = this.GetQueryRange(IID_Attack); // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that. this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"), false); if (enable) cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery); }; // FSM linkage functions // Setting the next state to the current state will leave/re-enter the top-most substate. // Must be called from inside the FSM. UnitAI.prototype.SetNextState = function(state) { this.UnitFsm.SetNextState(this, state); }; // Must be called from inside the FSM. UnitAI.prototype.DeferMessage = function(msg) { this.UnitFsm.DeferMessage(this, msg); }; UnitAI.prototype.GetCurrentState = function() { return this.UnitFsm.GetCurrentState(this); }; UnitAI.prototype.FsmStateNameChanged = function(state) { Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state }); }; /** * Call when the current order has been completed (or failed). * Removes the current order from the queue, and processes the * next one (if any). Returns false and defaults to IDLE * if there are no remaining orders or if the unit is not * inWorld and not garrisoned (thus usually waiting to be destroyed). * Must be called from inside the FSM. */ UnitAI.prototype.FinishOrder = function() { if (!this.orderQueue.length) { let stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetCurrentTemplateName(this.entity); error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack); } this.orderQueue.shift(); this.order = this.orderQueue[0]; if (this.orderQueue.length && (this.isGarrisoned || this.IsFormationController() || Engine.QueryInterface(this.entity, IID_Position)?.IsInWorld())) { let ret = this.UnitFsm.ProcessMessage(this, { "type": "Order."+this.order.type, "data": this.order.data }); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); return ret; } this.orderQueue = []; this.order = undefined; // Switch to IDLE as a default state. this.SetNextState("IDLE"); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // Check if there are queued formation orders if (this.IsFormationMember()) { this.SetNextState("FORMATIONMEMBER.IDLE"); let cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpUnitAI) { // Inform the formation controller that we finished this task Engine.QueryInterface(this.formationController, IID_Formation). SetFinishedEntity(this.entity); // We don't want to carry out the default order // if there are still queued formation orders left if (cmpUnitAI.GetOrders().length > 1) return true; } } return false; }; /** * Add an order onto the back of the queue, * and execute it if we didn't already have an order. */ UnitAI.prototype.PushOrder = function(type, data) { var order = { "type": type, "data": data }; this.orderQueue.push(order); if (this.orderQueue.length == 1) { this.order = order; this.UnitFsm.ProcessMessage(this, { "type": "Order."+this.order.type, "data": this.order.data }); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; /** * Add an order onto the front of the queue, * and execute it immediately. */ UnitAI.prototype.PushOrderFront = function(type, data, ignorePacking = false) { var order = { "type": type, "data": data }; // If current order is packing/unpacking then add new order after it. if (!ignorePacking && this.order && this.IsPacking()) { var packingOrder = this.orderQueue.shift(); this.orderQueue.unshift(packingOrder, order); } else { this.orderQueue.unshift(order); this.order = order; this.UnitFsm.ProcessMessage(this, { "type": "Order."+this.order.type, "data": this.order.data }); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; /** * Insert an order after the last forced order onto the queue * and after the other orders of the same type */ UnitAI.prototype.PushOrderAfterForced = function(type, data) { if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type)) this.PushOrderFront(type, data); else { for (let i = 1; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].data && this.orderQueue[i].data.force) continue; if (this.orderQueue[i].type == type) continue; this.orderQueue.splice(i, 0, { "type": type, "data": data }); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); return; } this.PushOrder(type, data); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; /** * For a unit that is packing and trying to attack something, * either cancel packing or continue with packing, as appropriate. * Precondition: if the unit is packing/unpacking, then orderQueue * should have the Attack order at index 0, * and the Pack/Unpack order at index 1. * This precondition holds because if we are packing while processing "Order.Attack", * then we must have come from ReplaceOrder, which guarantees it. * * @param {boolean} requirePacked - true if the unit needs to be packed to continue attacking, * false if it needs to be unpacked. * @return {boolean} true if the unit can attack now, false if it must continue packing (or unpacking) first. */ UnitAI.prototype.EnsureCorrectPackStateForAttack = function(requirePacked) { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (!cmpPack || !cmpPack.IsPacking() || this.orderQueue.length != 2 || this.orderQueue[0].type != "Attack" || this.orderQueue[1].type != "Pack" && this.orderQueue[1].type != "Unpack") return true; if (cmpPack.IsPacked() == requirePacked) { // The unit is already in the packed/unpacked state we want. // Delete the packing order. this.orderQueue.splice(1, 1); cmpPack.CancelPack(); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // Continue with the attack order. return true; } // Move the attack order behind the unpacking order, to continue unpacking. let tmp = this.orderQueue[0]; this.orderQueue[0] = this.orderQueue[1]; this.orderQueue[1] = tmp; Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); return false; }; UnitAI.prototype.WillMoveFromFoundation = function(target, checkPacking = true) { let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (!IsOwnedByAllyOfEntity(this.entity, target) && cmpUnitAI && !cmpUnitAI.IsAnimal() && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() || checkPacking && this.IsPacking() || this.CanPack() || !this.AbleToMove()) return false; return !this.CheckTargetRangeExplicit(target, g_LeaveFoundationRange, -1); }; UnitAI.prototype.ReplaceOrder = function(type, data) { // Remember the previous work orders to be able to go back to them later if required if (data && data.force) { if (this.IsFormationController()) this.CallMemberFunction("UpdateWorkOrders", [type]); else this.UpdateWorkOrders(type); } // Do not replace packing/unpacking unless it is cancel order. // TODO: maybe a better way of doing this would be to use priority levels if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack" && type != "Stop") { var order = { "type": type, "data": data }; var packingOrder = this.orderQueue.shift(); if (type == "Attack") { // The Attack order is able to handle a packing unit, while other orders can't. this.orderQueue = [packingOrder]; this.PushOrderFront(type, data, true); } else if (packingOrder.type == "Unpack" && g_OrdersCancelUnpacking.has(type)) { // Immediately cancel unpacking before processing an order that demands a packed unit. let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.CancelPack(); this.orderQueue = []; this.PushOrder(type, data); } else this.orderQueue = [packingOrder, order]; } else if (this.IsFormationMember()) { // Don't replace orders after a LeaveFormation order // (this is needed to support queued no-formation orders). let idx = this.orderQueue.findIndex(o => o.type == "LeaveFormation"); if (idx === -1) { this.orderQueue = []; this.order = undefined; } else this.orderQueue.splice(0, idx); this.PushOrderFront(type, data); } else { this.orderQueue = []; this.PushOrder(type, data); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.GetOrders = function() { return this.orderQueue.slice(); }; UnitAI.prototype.AddOrders = function(orders) { orders.forEach(order => this.PushOrder(order.type, order.data)); }; UnitAI.prototype.GetOrderData = function() { var orders = []; for (let order of this.orderQueue) if (order.data) orders.push(clone(order.data)); return orders; }; UnitAI.prototype.UpdateWorkOrders = function(type) { var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource"; if (isWorkType(type)) { this.workOrders = []; return; } if (this.workOrders.length) return; if (this.IsFormationMember()) { var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpUnitAI) { for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i) { if (isWorkType(cmpUnitAI.orderQueue[i].type)) { this.workOrders = cmpUnitAI.orderQueue.slice(i); return; } } } } // If nothing found, take the unit orders for (var i = 0; i < this.orderQueue.length; ++i) { if (isWorkType(this.orderQueue[i].type)) { this.workOrders = this.orderQueue.slice(i); return; } } }; UnitAI.prototype.BackToWork = function() { if (this.workOrders.length == 0) return false; if (this.isGarrisoned && !Engine.QueryInterface(this.entity, IID_Garrisonable)?.UnGarrison(false)) return false; const cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable); if (this.IsTurret(cmpTurretable) && !cmpTurretable.LeaveTurret()) return false; this.orderQueue = []; this.AddOrders(this.workOrders); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); if (this.IsFormationMember()) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers([this.entity]); } this.workOrders = []; return true; }; UnitAI.prototype.HasWorkOrders = function() { return this.workOrders.length > 0; }; UnitAI.prototype.GetWorkOrders = function() { return this.workOrders; }; UnitAI.prototype.SetWorkOrders = function(orders) { this.workOrders = orders; }; UnitAI.prototype.TimerHandler = function(data, lateness) { // Reset the timer if (data.timerRepeat === undefined) this.timer = undefined; this.UnitFsm.ProcessMessage(this, { "type": "Timer", "data": data, "lateness": lateness }); }; /** * Set up the UnitAI timer to run after 'offset' msecs, and then * every 'repeat' msecs until StopTimer is called. A "Timer" message * will be sent each time the timer runs. */ UnitAI.prototype.StartTimer = function(offset, repeat) { if (this.timer) error("Called StartTimer when there's already an active timer"); var data = { "timerRepeat": repeat }; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (repeat === undefined) this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data); else this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data); }; /** * Stop the current UnitAI timer. */ UnitAI.prototype.StopTimer = function() { if (!this.timer) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; UnitAI.prototype.OnMotionUpdate = function(msg) { if (msg.veryObstructed) msg.obstructed = true; this.UnitFsm.ProcessMessage(this, Object.assign({ "type": "MovementUpdate" }, msg)); }; /** * Called directly by cmpFoundation and cmpRepairable to * inform builders that repairing has finished. * This not done by listening to a global message due to performance. */ UnitAI.prototype.ConstructionFinished = function(msg) { this.UnitFsm.ProcessMessage(this, { "type": "ConstructionFinished", "data": msg }); }; UnitAI.prototype.OnGlobalEntityRenamed = function(msg) { let changed = false; let currentOrderChanged = false; for (let i = 0; i < this.orderQueue.length; ++i) { let order = this.orderQueue[i]; if (order.data && order.data.target && order.data.target == msg.entity) { changed = true; if (i == 0) currentOrderChanged = true; order.data.target = msg.newentity; } if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity) { changed = true; if (i == 0) currentOrderChanged = true; order.data.formationTarget = msg.newentity; } } if (!changed) return; if (currentOrderChanged) this.UnitFsm.ProcessMessage(this, { "type": "OrderTargetRenamed", "data": msg }); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.OnAttacked = function(msg) { if (msg.fromStatusEffect) return; this.UnitFsm.ProcessMessage(this, { "type": "Attacked", "data": msg }); }; UnitAI.prototype.OnGuardedAttacked = function(msg) { this.UnitFsm.ProcessMessage(this, { "type": "GuardedAttacked", "data": msg.data }); }; UnitAI.prototype.OnRangeUpdate = function(msg) { if (msg.tag == this.losRangeQuery) this.UnitFsm.ProcessMessage(this, { "type": "LosRangeUpdate", "data": msg }); else if (msg.tag == this.losHealRangeQuery) this.UnitFsm.ProcessMessage(this, { "type": "LosHealRangeUpdate", "data": msg }); else if (msg.tag == this.losAttackRangeQuery) this.UnitFsm.ProcessMessage(this, { "type": "LosAttackRangeUpdate", "data": msg }); }; UnitAI.prototype.OnPackFinished = function(msg) { this.UnitFsm.ProcessMessage(this, { "type": "PackFinished", "packed": msg.packed }); }; /** * A general function to process messages sent from components. * @param {string} type - The type of message to process. * @param {Object} msg - Optionally extra data to use. */ UnitAI.prototype.ProcessMessage = function(type, msg) { this.UnitFsm.ProcessMessage(this, { "type": type, "data": msg }); }; // Helper functions to be called by the FSM UnitAI.prototype.GetWalkSpeed = function() { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpUnitMotion) return 0; return cmpUnitMotion.GetWalkSpeed(); }; UnitAI.prototype.GetRunMultiplier = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpUnitMotion) return 0; return cmpUnitMotion.GetRunMultiplier(); }; /** * Returns true if the target exists and has non-zero hitpoints. */ UnitAI.prototype.TargetIsAlive = function(ent) { var cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) return true; var cmpHealth = QueryMiragedInterface(ent, IID_Health); return cmpHealth && cmpHealth.GetHitpoints() != 0; }; /** * Returns true if the target exists and needs to be killed before * beginning to gather resources from it. */ UnitAI.prototype.MustKillGatherTarget = function(ent) { var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (!cmpResourceSupply) return false; if (!cmpResourceSupply.GetKillBeforeGather()) return false; return this.TargetIsAlive(ent); }; /** * Returns the position of target or, if there is none, * the entity's position, or undefined. */ UnitAI.prototype.TargetPosOrEntPos = function(target) { let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (cmpTargetPosition && cmpTargetPosition.IsInWorld()) return cmpTargetPosition.GetPosition2D(); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) return cmpPosition.GetPosition2D(); return undefined; }; /** * Returns the entity ID of the nearest resource supply where the given * filter returns true, or undefined if none can be found. * "Nearest" is nearest from @param position. * TODO: extend this to exclude resources that already have lots of gatherers. */ UnitAI.prototype.FindNearbyResource = function(position, filter) { if (!position) return undefined; // We accept resources owned by Gaia or any player let players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); let range = 64; // TODO: what's a sensible number? let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); // Don't account for entity size, we need to match LOS visibility. let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_ResourceSupply, false); return nearby.find(ent => { if (!this.CanGather(ent) || !this.CheckTargetVisible(ent)) return false; let template = cmpTemplateManager.GetCurrentTemplateName(ent); if (template.indexOf("resource|") != -1) template = template.slice(9); let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); let type = cmpResourceSupply.GetType(); return cmpResourceSupply.IsAvailableTo(this.entity) && filter(ent, type, template); }); }; /** * Returns the entity ID of the nearest resource dropsite that accepts * the given type, or undefined if none can be found. */ UnitAI.prototype.FindNearestDropsite = function(genericType) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return undefined; let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return undefined; let pos = cmpPosition.GetPosition2D(); let bestDropsite; let bestDist = Infinity; // Maximum distance a point on an obstruction can be from the center of the obstruction. let maxDifference = 40; let owner = cmpOwnership.GetOwner(); - let cmpPlayer = QueryOwnerInterface(this.entity); - let players = cmpPlayer && cmpPlayer.HasSharedDropsites() ? cmpPlayer.GetMutualAllies() : [owner]; + let cmpDiplomacy = QueryOwnerInterface(this.entity, IID_Diplomacy); + let players = cmpDiplomacy && cmpDiplomacy.HasSharedDropsites() ? cmpDiplomacy.GetMutualAllies() : [owner]; let nearestDropsites = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite, false); let isShip = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship"); let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); for (let dropsite of nearestDropsites) { // Ships are unable to reach land dropsites and shouldn't attempt to do so. if (isShip && !Engine.QueryInterface(dropsite, IID_Identity).HasClass("Naval")) continue; let cmpResourceDropsite = Engine.QueryInterface(dropsite, IID_ResourceDropsite); if (!cmpResourceDropsite.AcceptsType(genericType) || !this.CheckTargetVisible(dropsite)) continue; if (Engine.QueryInterface(dropsite, IID_Ownership).GetOwner() != owner && !cmpResourceDropsite.IsShared()) continue; // The range manager sorts entities by the distance to their center, // but we want the distance to the point where resources will be dropped off. let dist = cmpObstructionManager.DistanceToPoint(dropsite, pos.x, pos.y); if (dist == -1) continue; if (dist < bestDist) { bestDropsite = dropsite; bestDist = dist; } else if (dist > bestDist + maxDifference) break; } return bestDropsite; }; /** * Returns the entity ID of the nearest building that needs to be constructed. * "Nearest" is nearest from @param position. */ UnitAI.prototype.FindNearbyFoundation = function(position) { if (!position) return undefined; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return undefined; let players = [cmpOwnership.GetOwner()]; let range = 64; // TODO: what's a sensible number? let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); // Don't account for entity size, we need to match LOS visibility. let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_Foundation, false); // Skip foundations that are already complete. (This matters since // we process the ConstructionFinished message before the foundation // we're working on has been deleted.) return nearby.find(ent => !Engine.QueryInterface(ent, IID_Foundation).IsFinished() && this.CheckTargetVisible(ent)); }; /** * Returns the entity ID of the nearest treasure. * "Nearest" is nearest from @param position. */ UnitAI.prototype.FindNearbyTreasure = function(position) { if (!position) return undefined; let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector); if (!cmpTreasureCollector) return undefined; let players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); let range = 64; // TODO: what's a sensible number? let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); // Don't account for entity size, we need to match LOS visibility. let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_Treasure, false); return nearby.find(ent => cmpTreasureCollector.CanCollect(ent) && this.CheckTargetVisible(ent)); }; /** * Play a sound appropriate to the current entity. */ UnitAI.prototype.PlaySound = function(name) { if (this.IsFormationController()) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); var member = cmpFormation.GetPrimaryMember(); if (member) PlaySound(name, member); } else { PlaySound(name, this.entity); } }; /* * Set a visualActor animation variant. * By changing the animation variant, you can change animations based on unitAI state. * If there are no specific variants or the variant doesn't exist in the actor, * the actor fallbacks to any existing animation. * @param type if present, switch to a specific animation variant. */ UnitAI.prototype.SetAnimationVariant = function(type) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetVariant("animationVariant", type); }; /* * Reset the animation variant to default behavior. * Default behavior is to pick a resource-carrying variant if resources are being carried. * Otherwise pick nothing in particular. */ UnitAI.prototype.SetDefaultAnimationVariant = function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) { let type = cmpResourceGatherer.GetLastCarriedType(); if (type) { let typename = "carry_" + type.generic; if (type.specific == "meat") typename = "carry_" + type.specific; this.SetAnimationVariant(typename); return; } } this.SetAnimationVariant(""); }; UnitAI.prototype.ResetAnimation = function() { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation("idle", false, 1.0); }; UnitAI.prototype.SelectAnimation = function(name, once = false, speed = 1.0) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation(name, once, speed); }; UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetAnimationSyncRepeat(repeattime); cmpVisual.SetAnimationSyncOffset(actiontime); }; UnitAI.prototype.StopMoving = function() { const cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpUnitMotion) return; cmpUnitMotion.StopMoving(); cmpUnitMotion.SetSpeedMultiplier(1); }; /** * Generic dispatcher for other MoveTo functions. * @param iid - Interface ID (optional) implementing GetRange * @param type - Range type for the interface call * @returns whether the move succeeded or failed. */ UnitAI.prototype.MoveTo = function(data, iid, type) { if (data.target) { if (data.min || data.max) return this.MoveToTargetRangeExplicit(data.target, data.min || -1, data.max || -1); else if (!iid) return this.MoveToTarget(data.target); return this.MoveToTargetRange(data.target, iid, type); } else if (data.min || data.max) return this.MoveToPointRange(data.x, data.z, data.min || -1, data.max || -1); return this.MoveToPoint(data.x, data.z); }; UnitAI.prototype.MoveToPoint = function(x, z) { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, 0, 0); // For point goals, allow a max range of 0. }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; UnitAI.prototype.MoveToTarget = function(target) { if (!this.CheckTargetVisible(target)) return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, 0, 1); }; UnitAI.prototype.MoveToTargetRange = function(target, iid, type) { if (!this.CheckTargetVisible(target)) return false; let range = this.GetRange(iid, type, target); if (!range) return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; /** * Move unit so we hope the target is in the attack range * for melee attacks, this goes straight to the default range checks * for ranged attacks, the parabolic range is used */ UnitAI.prototype.MoveToTargetAttackRange = function(target, type) { // for formation members, the formation will take care of the range check if (this.IsFormationMember()) { let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation()) return false; } const cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!this.AbleToMove(cmpUnitMotion)) return false; const cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); if (type != "Ranged") return this.MoveToTargetRange(target, IID_Attack, type); if (!this.CheckTargetVisible(target)) return false; const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; const range = cmpAttack.GetRange(type); // In case the range returns negative, we are probably too high compared to the target. Hope we come close enough. const parabolicMaxRange = Math.max(0, Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEffectiveParabolicRange(this.entity, target, range.max, cmpAttack.GetAttackYOrigin(type))); // The parabole changes while walking so be cautious: const guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange; return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange); }; UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) { if (!this.CheckTargetVisible(target)) return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, min, max); }; /** * Move unit so we hope the target is in the attack range of the formation. * * @param {number} target - The target entity ID to attack. * @return {boolean} - Whether the order to move has succeeded. */ UnitAI.prototype.MoveFormationToTargetAttackRange = function(target) { let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation); if (cmpTargetFormation) target = cmpTargetFormation.GetClosestMember(this.entity); if (!this.CheckTargetVisible(target)) return false; let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpFormationAttack) return false; let range = cmpFormationAttack.GetRange(target); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; /** * Generic dispatcher for other Check...Range functions. * @param iid - Interface ID (optional) implementing GetRange * @param type - Range type for the interface call */ UnitAI.prototype.CheckRange = function(data, iid, type) { if (data.target) { if (data.min || data.max) return this.CheckTargetRangeExplicit(data.target, data.min || -1, data.max || -1); else if (!iid) return this.CheckTargetRangeExplicit(data.target, 0, 1); return this.CheckTargetRange(data.target, iid, type); } else if (data.min || data.max) return this.CheckPointRangeExplicit(data.x, data.z, data.min || -1, data.max || -1); return this.CheckPointRangeExplicit(data.x, data.z, 0, 0); }; UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max) { let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, false); }; UnitAI.prototype.CheckTargetRange = function(target, iid, type) { let range = this.GetRange(iid, type, target); if (!range) return false; let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); }; /** * Check if the target is inside the attack range * For melee attacks, this goes straigt to the regular range calculation * For ranged attacks, the parabolic formula is used to accout for bigger ranges * when the target is lower, and smaller ranges when the target is higher */ UnitAI.prototype.CheckTargetAttackRange = function(target, type) { // for formation members, the formation will take care of the range check if (this.IsFormationMember()) { let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation() && cmpFormationUnitAI.order.data.target == target) return true; } let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); return cmpAttack && cmpAttack.IsTargetInRange(target, type); }; UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) { let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, false); }; /** * Check if the target is inside the attack range of the formation. * * @param {number} target - The target entity ID to attack. * @return {boolean} - Whether the entity is within attacking distance. */ UnitAI.prototype.CheckFormationTargetAttackRange = function(target) { let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation); if (cmpTargetFormation) target = cmpTargetFormation.GetClosestMember(this.entity); let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpFormationAttack) return false; let range = cmpFormationAttack.GetRange(target); let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); }; /** * Returns true if the target entity is visible through the FoW/SoD. */ UnitAI.prototype.CheckTargetVisible = function(target) { if (this.isGarrisoned) return false; const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return false; // Entities that are hidden and miraged are considered visible const cmpFogging = Engine.QueryInterface(target, IID_Fogging); if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner())) return true; if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden") return false; // Either visible directly, or visible in fog return true; }; /** * Returns true if the given position is currentl visible (not in FoW/SoD). */ UnitAI.prototype.CheckPositionVisible = function(x, z) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return false; return cmpRangeManager.GetLosVisibilityPosition(x, z, cmpOwnership.GetOwner()) == "visible"; }; /** * How close to our goal do we consider it's OK to stop if the goal appears unreachable. * Currently 3 terrain tiles as that's relatively close but helps pathfinding. */ UnitAI.prototype.DefaultRelaxedMaxRange = 12; /** * @returns true if the unit is in the relaxed-range from the target. */ UnitAI.prototype.RelaxedMaxRangeCheck = function(data, relaxedRange) { if (!data.relaxed) return false; let ndata = data; ndata.min = 0; ndata.max = relaxedRange; return this.CheckRange(ndata); }; /** * Let an entity face its target. * @param {number} target - The entity-ID of the target. */ UnitAI.prototype.FaceTowardsTarget = function(target) { let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; let targetPosition = cmpTargetPosition.GetPosition2D(); // Use cmpUnitMotion for units that support that, otherwise try cmpPosition (e.g. turrets) let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion) { cmpUnitMotion.FaceTowardsPoint(targetPosition.x, targetPosition.y); return; } let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) cmpPosition.TurnTo(cmpPosition.GetPosition2D().angleTo(targetPosition)); }; UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type) { let range = this.GetRange(iid, type, target); if (!range) return false; let cmpPosition = Engine.QueryInterface(target, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; let cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; let halfvision = cmpVision.GetRange() / 2; let pos = cmpPosition.GetPosition(); let heldPosition = this.heldPosition; if (heldPosition === undefined) heldPosition = { "x": pos.x, "z": pos.z }; return Math.euclidDistance2D(pos.x, pos.z, heldPosition.x, heldPosition.z) < halfvision + range.max; }; UnitAI.prototype.CheckTargetIsInVisionRange = function(target) { let cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; let range = cmpVision.GetRange(); let distance = PositionHelper.DistanceBetweenEntities(this.entity, target); return distance < range; }; UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture = this.DEFAULT_CAPTURE) { return Engine.QueryInterface(this.entity, IID_Attack)?.GetBestAttackAgainst(target, allowCapture); }; /** * Try to find one of the given entities which can be attacked, * and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackVisibleEntity = function(ents) { var target = ents.find(target => this.CanAttack(target)); if (!target) return false; this.PushOrderFront("Attack", { "target": target, "force": false }); return true; }; /** * Try to find one of the given entities which can be attacked * and which is close to the hold position, and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackEntityInZone = function(ents) { var target = ents.find(target => this.CanAttack(target) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true)) && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target)) ); if (!target) return false; this.PushOrderFront("Attack", { "target": target, "force": false }); return true; }; /** * Try to respond appropriately given our current stance, * given a list of entities that match our stance's target criteria. * Returns true if it responded. */ UnitAI.prototype.RespondToTargetedEntities = function(ents) { if (!ents.length) return false; if (this.GetStance().respondChase) return this.AttackVisibleEntity(ents); if (this.GetStance().respondStandGround) return this.AttackVisibleEntity(ents); if (this.GetStance().respondHoldGround) return this.AttackEntityInZone(ents); if (this.GetStance().respondFlee) { if (this.order && this.order.type == "Flee") this.orderQueue.shift(); this.PushOrderFront("Flee", { "target": ents[0], "force": false }); return true; } return false; }; /** * @param {number} ents - An array of the IDs of the spotted entities. * @return {boolean} - Whether we responded. */ UnitAI.prototype.RespondToSightedEntities = function(ents) { if (!ents || !ents.length) return false; if (this.GetStance().respondFleeOnSight) { this.Flee(ents[0], false); return true; } return false; }; /** * Try to respond to healable entities. * Returns true if it responded. */ UnitAI.prototype.RespondToHealableEntities = function(ents) { let ent = ents.find(ent => this.CanHeal(ent)); if (!ent) return false; this.PushOrderFront("Heal", { "target": ent, "force": false }); return true; }; /** * Returns true if we should stop following the target entity. */ UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type) { if (!this.CheckTargetVisible(target)) return true; // Forced orders shouldn't be interrupted. if (force) return false; // If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker if (this.isGuardOf) { let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); let cmpAttack = Engine.QueryInterface(target, IID_Attack); if (cmpUnitAI && cmpAttack && cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type))) return false; } if (this.GetStance().respondHoldGround) if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type)) return true; // Stop if it's left our vision range, unless we're especially persistent. if (!this.GetStance().respondChaseBeyondVision) if (!this.CheckTargetIsInVisionRange(target)) return true; return false; }; /* * Returns whether we should chase the targeted entity, * given our current stance. */ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force) { if (!this.AbleToMove()) return false; if (this.GetStance().respondChase) return true; // If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker if (this.isGuardOf) { let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); let cmpAttack = Engine.QueryInterface(target, IID_Attack); if (cmpUnitAI && cmpAttack && cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type))) return true; } return force; }; // External interface functions /** * Order a unit to leave the formation it is in. * Used to handle queued no-formation orders for units in formation. */ UnitAI.prototype.LeaveFormation = function(queued = true) { // If queued, add the order even if we're not in formation, // maybe we will be later. if (!queued && !this.IsFormationMember()) return; if (queued) this.AddOrder("LeaveFormation", { "force": true }, queued); else this.PushOrderFront("LeaveFormation", { "force": true }); }; UnitAI.prototype.SetFormationController = function(ent) { this.formationController = ent; // Set obstruction group, so we can walk through members of our own formation. Engine.QueryInterface(this.entity, IID_Obstruction)?.SetControlGroup(ent); Engine.QueryInterface(this.entity, IID_UnitMotion)?.SetMemberOfFormation(ent); }; UnitAI.prototype.UnsetFormationController = function() { this.formationController = INVALID_ENTITY; Engine.QueryInterface(this.entity, IID_Obstruction)?.SetControlGroup(this.entity); Engine.QueryInterface(this.entity, IID_UnitMotion)?.SetMemberOfFormation(this.formationController); this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" }); }; UnitAI.prototype.GetFormationController = function() { return this.formationController; }; UnitAI.prototype.GetFormationTemplate = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || NULL_FORMATION; }; UnitAI.prototype.MoveIntoFormation = function(cmd) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true }); }; UnitAI.prototype.GetTargetPositions = function() { var targetPositions = []; for (var i = 0; i < this.orderQueue.length; ++i) { var order = this.orderQueue[i]; switch (order.type) { case "Walk": case "WalkAndFight": case "WalkToPointRange": case "MoveIntoFormation": case "GatherNearPosition": case "Patrol": targetPositions.push(new Vector2D(order.data.x, order.data.z)); break; // and continue the loop case "WalkToTarget": case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will. case "Guard": case "Flee": case "LeaveFoundation": case "Attack": case "Heal": case "Gather": case "ReturnResource": case "Repair": case "Garrison": case "CollectTreasure": var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return targetPositions; targetPositions.push(cmpTargetPosition.GetPosition2D()); return targetPositions; case "Stop": return []; case "DropAtNearestDropSite": break; default: error("GetTargetPositions: Unrecognised order type '"+order.type+"'"); return []; } } return targetPositions; }; /** * Returns the estimated distance that this unit will travel before either * finishing all of its orders, or reaching a non-walk target (attack, gather, etc). * Intended for Formation to switch to column layout on long walks. */ UnitAI.prototype.ComputeWalkingDistance = function() { var distance = 0; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return 0; // Keep track of the position at the start of each order var pos = cmpPosition.GetPosition2D(); var targetPositions = this.GetTargetPositions(); for (var i = 0; i < targetPositions.length; ++i) { distance += pos.distanceTo(targetPositions[i]); // Remember this as the start position for the next order pos = targetPositions[i]; } return distance; }; UnitAI.prototype.AddOrder = function(type, data, queued, pushFront) { if (this.expectedRoute) this.expectedRoute = undefined; if (pushFront) this.PushOrderFront(type, data); else if (queued) this.PushOrder(type, data); else this.ReplaceOrder(type, data); }; /** * Adds guard/escort order to the queue, forced by the player. */ UnitAI.prototype.Guard = function(target, queued, pushFront) { if (!this.CanGuard()) { this.WalkToTarget(target, queued); return; } if (target === this.entity) return; if (this.isGuardOf) { if (this.isGuardOf == target && this.order && this.order.type == "Guard") return; this.RemoveGuard(); } this.AddOrder("Guard", { "target": target, "force": false }, queued, pushFront); }; /** * @return {boolean} - Whether it makes sense to guard the given entity. */ UnitAI.prototype.ShouldGuard = function(target) { return this.TargetIsAlive(target) || Engine.QueryInterface(target, IID_Capturable) || Engine.QueryInterface(target, IID_StatusEffectsReceiver); }; UnitAI.prototype.AddGuard = function(target) { if (!this.CanGuard()) return false; var cmpGuard = Engine.QueryInterface(target, IID_Guard); if (!cmpGuard) return false; this.isGuardOf = target; this.guardRange = cmpGuard.GetRange(this.entity); cmpGuard.AddGuard(this.entity); return true; }; UnitAI.prototype.RemoveGuard = function() { if (!this.isGuardOf) return; let cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard); if (cmpGuard) cmpGuard.RemoveGuard(this.entity); this.guardRange = undefined; this.isGuardOf = undefined; if (!this.order) return; if (this.order.type == "Guard") this.UnitFsm.ProcessMessage(this, { "type": "RemoveGuard" }); else for (let i = 1; i < this.orderQueue.length; ++i) if (this.orderQueue[i].type == "Guard") this.orderQueue.splice(i, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.IsGuardOf = function() { return this.isGuardOf; }; UnitAI.prototype.SetGuardOf = function(entity) { // entity may be undefined this.isGuardOf = entity; }; UnitAI.prototype.CanGuard = function() { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; return this.template.CanGuard == "true"; }; UnitAI.prototype.CanPatrol = function() { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) return this.IsFormationController() || this.template.CanPatrol == "true"; }; /** * Adds walk order to queue, forced by the player. */ UnitAI.prototype.Walk = function(x, z, queued, pushFront) { if (!pushFront && this.expectedRoute && queued) this.expectedRoute.push({ "x": x, "z": z }); else this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued, pushFront); }; /** * Adds walk to point range order to queue, forced by the player. */ UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued, pushFront) { this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued, pushFront); }; /** * Adds stop order to queue, forced by the player. */ UnitAI.prototype.Stop = function(queued, pushFront) { this.AddOrder("Stop", { "force": true }, queued, pushFront); }; /** * The unit will drop all resources at the closest dropsite. If this unit is no gatherer or * no dropsite is available, it will do nothing. */ UnitAI.prototype.DropAtNearestDropSite = function(queued, pushFront) { this.AddOrder("DropAtNearestDropSite", { "force": true }, queued, pushFront); }; /** * Adds walk-to-target order to queue, this only occurs in response * to a player order, and so is forced. */ UnitAI.prototype.WalkToTarget = function(target, queued, pushFront) { this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued, pushFront); }; /** * Adds walk-and-fight order to queue, this only occurs in response * to a player order, and so is forced. * If targetClasses is given, only entities matching the targetClasses can be attacked. */ UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false) { this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued, pushFront); }; UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false) { if (!this.CanPatrol()) { this.Walk(x, z, queued); return; } this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued, pushFront); }; /** * Adds leave foundation order to queue, treated as forced. */ UnitAI.prototype.LeaveFoundation = function(target) { // If we're already being told to leave a foundation, then // ignore this new request so we don't end up being too indecisive // to ever actually move anywhere. if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target))) return; if (this.orderQueue.length && this.orderQueue[0].type == "Unpack" && this.WillMoveFromFoundation(target, false)) { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack) cmpPack.CancelPack(); } if (this.IsPacking()) return; this.PushOrderFront("LeaveFoundation", { "target": target, "force": true }); }; /** * Adds attack order to the queue, forced by the player. */ UnitAI.prototype.Attack = function(target, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false) { if (!this.CanAttack(target)) { // We don't want to let healers walk to the target unit so they can be easily killed. // Instead we just let them get into healing range. if (this.IsHealer()) this.MoveToTargetRange(target, IID_Heal); else this.WalkToTarget(target, queued, pushFront); return; } let order = { "target": target, "force": true, "allowCapture": allowCapture, }; this.RememberTargetPosition(order); if (this.order && this.order.type == "Attack" && this.order.data && this.order.data.target === order.target && this.order.data.allowCapture === order.allowCapture) { this.order.data.lastPos = order.lastPos; this.order.data.force = order.force; if (order.force) this.orderQueue = [this.order]; return; } this.AddOrder("Attack", order, queued, pushFront); }; /** * Adds garrison order to the queue, forced by the player. */ UnitAI.prototype.Garrison = function(target, queued, pushFront) { // Not allowed to garrison when occupying a turret, at the moment. if (this.isGarrisoned || this.IsTurret()) return; if (target == this.entity) return; if (!this.CanGarrison(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Garrison", { "target": target, "force": true, "garrison": true }, queued, pushFront); }; /** * Adds ungarrison order to the queue. */ UnitAI.prototype.Ungarrison = function() { if (!this.isGarrisoned && !this.IsTurret()) return; this.AddOrder("Ungarrison", null, false); }; /** * Adds garrison order to the queue, forced by the player. */ UnitAI.prototype.OccupyTurret = function(target, queued, pushFront) { if (target == this.entity) return; if (!this.CanOccupyTurret(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Garrison", { "target": target, "force": true, "garrison": false }, queued, pushFront); }; /** * Adds gather order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Gather = function(target, queued, pushFront) { this.PerformGather(target, queued, true, pushFront); }; /** * Internal function to abstract the force parameter. */ UnitAI.prototype.PerformGather = function(target, queued, force, pushFront = false) { if (!this.CanGather(target)) { this.WalkToTarget(target, queued); return; } // Save the resource type now, so if the resource gets destroyed // before we process the order then we still know what resource // type to look for more of var type; var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply); if (cmpResourceSupply) type = cmpResourceSupply.GetType(); else error("CanGather allowed gathering from invalid entity"); // Also save the target entity's template, so that if it's an animal, // we won't go from hunting slow safe animals to dangerous fast ones var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(target); if (template.indexOf("resource|") != -1) template = template.slice(9); let order = { "target": target, "type": type, "template": template, "force": force, }; this.RememberTargetPosition(order); order.initPos = order.lastPos; if (this.order && (this.order.type == "Gather" || this.order.type == "Attack") && this.order.data && this.order.data.target === order.target) { this.order.data.lastPos = order.lastPos; this.order.data.force = order.force; if (order.force) { if (this.orderQueue[1]?.type === "Gather") this.orderQueue = [this.order, this.orderQueue[1]]; else this.orderQueue = [this.order]; } return; } this.AddOrder("Gather", order, queued, pushFront); }; /** * Adds gather-near-position order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued, pushFront) { if (template.indexOf("resource|") != -1) template = template.slice(9); if (this.IsFormationController() || Engine.QueryInterface(this.entity, IID_ResourceGatherer)) this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued, pushFront); else this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued, pushFront); }; /** * Adds heal order to the queue, forced by the player. */ UnitAI.prototype.Heal = function(target, queued, pushFront) { if (!this.CanHeal(target)) { this.WalkToTarget(target, queued); return; } if (this.order && this.order.type == "Heal" && this.order.data && this.order.data.target === target) { this.order.data.force = true; this.orderQueue = [this.order]; return; } this.AddOrder("Heal", { "target": target, "force": true }, queued, pushFront); }; /** * Adds return resource order to the queue, forced by the player. */ UnitAI.prototype.ReturnResource = function(target, queued, pushFront) { if (!this.CanReturnResource(target, true)) { this.WalkToTarget(target, queued); return; } this.AddOrder("ReturnResource", { "target": target, "force": true }, queued, pushFront); }; /** * Adds order to collect a treasure to queue, forced by the player. */ UnitAI.prototype.CollectTreasure = function(target, queued, pushFront) { this.AddOrder("CollectTreasure", { "target": target, "force": true }, queued, pushFront); }; /** * Adds order to collect a treasure to queue, forced by the player. */ UnitAI.prototype.CollectTreasureNearPosition = function(posX, posZ, queued, pushFront) { this.AddOrder("CollectTreasureNearPosition", { "x": posX, "z": posZ, "force": true }, queued, pushFront); }; UnitAI.prototype.CancelSetupTradeRoute = function(target) { let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader) return; cmpTrader.RemoveTargetMarket(target); if (this.IsFormationController()) this.CallMemberFunction("CancelSetupTradeRoute", [target]); }; /** * Adds trade order to the queue. Either walk to the first market, or * start a new route. Not forced, so it can be interrupted by attacks. * The possible route may be given directly as a SetupTradeRoute argument * if coming from a RallyPoint, or through this.expectedRoute if a user command. */ UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued, pushFront) { if (!this.CanTrade(target)) { this.WalkToTarget(target, queued); return; } // AI has currently no access to BackToWork let cmpPlayer = QueryOwnerInterface(this.entity); if (cmpPlayer && cmpPlayer.IsAI() && !this.IsFormationController() && this.workOrders.length && this.workOrders[0].type == "Trade") { let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader.HasBothMarkets() && (cmpTrader.GetFirstMarket() == target && cmpTrader.GetSecondMarket() == source || cmpTrader.GetFirstMarket() == source && cmpTrader.GetSecondMarket() == target)) { this.BackToWork(); return; } } var marketsChanged = this.SetTargetMarket(target, source); if (!marketsChanged) return; var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader.HasBothMarkets()) { let data = { "target": cmpTrader.GetFirstMarket(), "route": route, "force": false }; if (this.expectedRoute) { if (!route && this.expectedRoute.length) data.route = this.expectedRoute.slice(); this.expectedRoute = undefined; } if (this.IsFormationController()) { this.CallMemberFunction("AddOrder", ["Trade", data, queued]); let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.Disband(); } else this.AddOrder("Trade", data, queued, pushFront); } else { if (this.IsFormationController()) this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued, pushFront]); else this.WalkToTarget(cmpTrader.GetFirstMarket(), queued, pushFront); this.expectedRoute = []; } }; UnitAI.prototype.SetTargetMarket = function(target, source) { var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader) return false; var marketsChanged = cmpTrader.SetTargetMarket(target, source); if (this.IsFormationController()) this.CallMemberFunction("SetTargetMarket", [target, source]); return marketsChanged; }; UnitAI.prototype.SwitchMarketOrder = function(oldMarket, newMarket) { if (this.order && this.order.data && this.order.data.target && this.order.data.target == oldMarket) this.order.data.target = newMarket; }; UnitAI.prototype.MoveToMarket = function(targetMarket) { let nextTarget; if (this.waypoints && this.waypoints.length >= 1) nextTarget = this.waypoints.pop(); else nextTarget = { "target": targetMarket }; this.order.data.nextTarget = nextTarget; return this.MoveTo(this.order.data.nextTarget, IID_Trader); }; UnitAI.prototype.MarketRemoved = function(market) { if (this.order && this.order.data && this.order.data.target && this.order.data.target == market) this.UnitFsm.ProcessMessage(this, { "type": "TradingCanceled", "market": market }); }; /** * Adds repair/build order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Repair = function(target, autocontinue, queued, pushFront) { if (!this.CanRepair(target)) { this.WalkToTarget(target, queued); return; } if (this.order && this.order.type == "Repair" && this.order.data && this.order.data.target === target && this.order.data.autocontinue === autocontinue) { this.order.data.force = true; this.orderQueue = [this.order]; return; } this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued, pushFront); }; /** * Adds flee order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.Flee = function(target, queued, pushFront) { this.AddOrder("Flee", { "target": target, "force": false }, queued, pushFront); }; UnitAI.prototype.Cheer = function() { this.PushOrderFront("Cheer", { "force": false }); }; UnitAI.prototype.Pack = function(queued, pushFront) { if (this.CanPack()) this.AddOrder("Pack", { "force": true }, queued, pushFront); }; UnitAI.prototype.Unpack = function(queued, pushFront) { if (this.CanUnpack()) this.AddOrder("Unpack", { "force": true }, queued, pushFront); }; UnitAI.prototype.CancelPack = function(queued, pushFront) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) this.AddOrder("CancelPack", { "force": true }, queued, pushFront); }; UnitAI.prototype.CancelUnpack = function(queued, pushFront) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) this.AddOrder("CancelUnpack", { "force": true }, queued, pushFront); }; UnitAI.prototype.SetStance = function(stance) { if (g_Stances[stance]) { this.stance = stance; Engine.PostMessage(this.entity, MT_UnitStanceChanged, { "to": this.stance }); } else error("UnitAI: Setting to invalid stance '"+stance+"'"); }; UnitAI.prototype.SwitchToStance = function(stance) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.SetHeldPosition(pos.x, pos.z); this.SetStance(stance); // Reset the range queries, since the range depends on stance. this.SetupRangeQueries(); }; UnitAI.prototype.SetTurretStance = function() { this.SetImmobile(); this.previousStance = undefined; if (this.GetStance().respondStandGround) return; for (let stance in g_Stances) { if (!g_Stances[stance].respondStandGround) continue; this.previousStance = this.GetStanceName(); this.SwitchToStance(stance); return; } }; UnitAI.prototype.ResetTurretStance = function() { this.SetMobile(); if (!this.previousStance) return; this.SwitchToStance(this.previousStance); this.previousStance = undefined; }; /** * Resets the losRangeQuery. * @return {boolean} - Whether there are targets in range that we ought to react upon. */ UnitAI.prototype.FindSightedEnemies = function() { if (!this.losRangeQuery) return false; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return this.RespondToSightedEntities(cmpRangeManager.ResetActiveQuery(this.losRangeQuery)); }; /** * Resets losHealRangeQuery, and if there are some targets in range that we can heal * then we start healing and this returns true; otherwise, returns false. */ UnitAI.prototype.FindNewHealTargets = function() { if (!this.losHealRangeQuery) return false; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return this.RespondToHealableEntities(cmpRangeManager.ResetActiveQuery(this.losHealRangeQuery)); }; /** * Resets losAttackRangeQuery, and if there are some targets in range that we can * attack then we start attacking and this returns true; otherwise, returns false. */ UnitAI.prototype.FindNewTargets = function() { if (!this.losAttackRangeQuery) return false; if (!this.GetStance().targetVisibleEnemies) return false; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return this.AttackEntitiesByPreference(cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery)); }; UnitAI.prototype.FindWalkAndFightTargets = function() { if (this.IsFormationController()) return this.CallMemberFunction("FindWalkAndFightTargets", null); let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); let entities; if (!this.losAttackRangeQuery || !this.GetStance().targetVisibleEnemies || !cmpAttack) entities = []; else { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery); } let attackfilter = e => { if (this?.order?.data?.targetClasses) { let cmpIdentity = Engine.QueryInterface(e, IID_Identity); let targetClasses = this.order.data.targetClasses; if (cmpIdentity && targetClasses.attack && !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) return false; if (cmpIdentity && targetClasses.avoid && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) return false; // Only used by the AIs to prevent some choices of targets if (targetClasses.vetoEntities && targetClasses.vetoEntities[e]) return false; } let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() > 0) return true; let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }; const attack = target => { const order = { "target": target, "force": false, "allowCapture": this.order?.data?.allowCapture || this.DEFAULT_CAPTURE }; if (this.IsFormationMember()) this.ReplaceOrder("Attack", order); else this.PushOrderFront("Attack", order); }; let prefs = {}; let bestPref; let targets = []; let pref; for (let v of entities) { if (this.CanAttack(v) && attackfilter(v)) { pref = cmpAttack.GetPreference(v); if (pref === 0) { attack(v); return true; } targets.push(v); } prefs[v] = pref; if (pref !== undefined && (bestPref === undefined || pref < bestPref)) bestPref = pref; } for (let targ of targets) { if (prefs[targ] !== bestPref) continue; attack(targ); return true; } // healers on a walk-and-fight order should heal injured units if (this.IsHealer()) return this.FindNewHealTargets(); return false; }; UnitAI.prototype.GetQueryRange = function(iid) { let ret = { "min": 0, "max": 0 }; let cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; let visionRange = cmpVision.GetRange(); if (iid === IID_Vision) { ret.max = visionRange; return ret; } if (this.GetStance().respondStandGround) { let range = this.GetRange(iid); if (!range) return ret; ret.min = range.min; ret.max = Math.min(range.max, visionRange); } else if (this.GetStance().respondChase) ret.max = visionRange; else if (this.GetStance().respondHoldGround) { let range = this.GetRange(iid); if (!range) return ret; ret.max = Math.min(range.max + visionRange / 2, visionRange); } // We probably have stance 'passive' and we wouldn't have a range, // but as it is the default for healers we need to set it to something sane. else if (iid === IID_Heal) ret.max = visionRange; return ret; }; UnitAI.prototype.GetStance = function() { return g_Stances[this.stance]; }; UnitAI.prototype.GetSelectableStances = function() { if (this.IsTurret()) return []; return Object.keys(g_Stances).filter(key => g_Stances[key].selectable); }; UnitAI.prototype.GetStanceName = function() { return this.stance; }; /* * Make the unit run. */ UnitAI.prototype.Run = function() { this.SetSpeedMultiplier(this.GetRunMultiplier()); }; /** * @param {number} speed - The multiplier to set the speed to. */ UnitAI.prototype.SetSpeedMultiplier = function(speed) { Engine.QueryInterface(this.entity, IID_UnitMotion)?.SetSpeedMultiplier(speed); }; /** * Try to match the targets current movement speed. * * @param {number} target - The entity ID of the target to match. * @param {boolean} mayRun - Whether the entity is allowed to run to match the speed. */ UnitAI.prototype.TryMatchTargetSpeed = function(target, mayRun = true) { let cmpUnitMotionTarget = Engine.QueryInterface(target, IID_UnitMotion); if (cmpUnitMotionTarget) { let targetSpeed = cmpUnitMotionTarget.GetCurrentSpeed(); if (targetSpeed) this.SetSpeedMultiplier(Math.min(mayRun ? this.GetRunMultiplier() : 1, targetSpeed / this.GetWalkSpeed())); } }; /* * Remember the position of the target (in lastPos), if any, in case it disappears later * and we want to head to its last known position. * @param orderData - The order data to set this on. Defaults to this.order.data */ UnitAI.prototype.RememberTargetPosition = function(orderData) { if (!orderData) orderData = this.order.data; let cmpPosition = Engine.QueryInterface(orderData.target, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) orderData.lastPos = cmpPosition.GetPosition(); }; UnitAI.prototype.SetHeldPosition = function(x, z) { this.heldPosition = { "x": x, "z": z }; }; UnitAI.prototype.SetHeldPositionOnEntity = function(entity) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.SetHeldPosition(pos.x, pos.z); }; UnitAI.prototype.GetHeldPosition = function() { return this.heldPosition; }; UnitAI.prototype.WalkToHeldPosition = function() { if (this.heldPosition) { this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false, false); return true; } return false; }; // Helper functions /** * General getter for ranges. * * @param {number} iid * @param {number} target - [Optional] * @param {string} type - [Optional] * @return {Object | undefined} - The range in the form * { "min": number, "max": number } * Returns undefined when the entity does not have the requested component. */ UnitAI.prototype.GetRange = function(iid, type, target) { let component = Engine.QueryInterface(this.entity, iid); if (!component) return undefined; return component.GetRange(type, target); }; UnitAI.prototype.CanAttack = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(target); }; UnitAI.prototype.CanGarrison = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds). if (this.IsFormationController()) return true; let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable); return cmpGarrisonable && cmpGarrisonable.CanGarrison(target); }; UnitAI.prototype.CanGather = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds). if (this.IsFormationController()) return true; let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); return cmpResourceGatherer && cmpResourceGatherer.CanGather(target); }; UnitAI.prototype.CanHeal = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); return cmpHeal && cmpHeal.CanHeal(target); }; /** * Check if the entity can return carried resources at @param target * @param checkCarriedResource check we are carrying resources * @param cmpResourceGatherer if present, use this directly instead of re-querying. */ UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource, cmpResourceGatherer = undefined) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds). if (this.IsFormationController()) return true; if (!cmpResourceGatherer) cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); return cmpResourceGatherer && cmpResourceGatherer.CanReturnResource(target, checkCarriedResource); }; UnitAI.prototype.CanTrade = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds). if (this.IsFormationController()) return true; let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); return cmpTrader && cmpTrader.CanTrade(target); }; UnitAI.prototype.CanRepair = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds). if (this.IsFormationController()) return true; let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); return cmpBuilder && cmpBuilder.CanRepair(target); }; UnitAI.prototype.CanOccupyTurret = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds). if (this.IsFormationController()) return true; let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable); return cmpTurretable && cmpTurretable.CanOccupy(target); }; UnitAI.prototype.CanPack = function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return cmpPack && cmpPack.CanPack(); }; UnitAI.prototype.CanUnpack = function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return cmpPack && cmpPack.CanUnpack(); }; UnitAI.prototype.IsPacking = function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return cmpPack && cmpPack.IsPacking(); }; // Formation specific functions UnitAI.prototype.IsAttackingAsFormation = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttackAsFormation() && this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING"; }; UnitAI.prototype.MoveRandomly = function(distance) { // To minimize drift all across the map, describe circles // approximated by polygons. // And to avoid getting stuck in obstacles or narrow spaces, each side // of the polygon is obtained by trying to go away from a point situated // half a meter backwards of the current position, after rotation. // We also add a fluctuation on the length of each side of the polygon (dist) // which, in addition to making the move more random, helps escaping narrow spaces // with bigger values of dist. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion) return; let pos = cmpPosition.GetPosition(); let ang = cmpPosition.GetRotation().y; if (!this.roamAngle) { this.roamAngle = (randBool() ? 1 : -1) * Math.PI / 6; ang -= this.roamAngle / 2; this.startAngle = ang; } else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2)) this.roamAngle *= randBool() ? 1 : -1; let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4); // First half rotation to decrease the impression of immediate rotation ang += halfDelta; cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang)); // Then second half of the rotation ang += halfDelta; let dist = randFloat(0.5, 1.5) * distance; cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, -1); }; UnitAI.prototype.SetFacePointAfterMove = function(val) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpMotion) cmpMotion.SetFacePointAfterMove(val); }; UnitAI.prototype.GetFacePointAfterMove = function() { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion && cmpUnitMotion.GetFacePointAfterMove(); }; UnitAI.prototype.AttackEntitiesByPreference = function(ents) { if (!ents.length) return false; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; let attackfilter = function(e) { if (!cmpAttack.CanAttack(e)) return false; let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() > 0) return true; let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }; let entsByPreferences = {}; let preferences = []; let entsWithoutPref = []; for (let ent of ents) { if (!attackfilter(ent)) continue; let pref = cmpAttack.GetPreference(ent); // If we match our best preference, we can try responding right away. // This makes some common cases fast, like most soldiers having 'Human' as best preference, // or ships having 'Ship'. And if there are no such targets, this doesn't do much more work. if (pref === 0) { if (this.RespondToTargetedEntities([ent])) return true; } else if (pref === null || pref === undefined) entsWithoutPref.push(ent); else if (!entsByPreferences[pref]) { preferences.push(pref); entsByPreferences[pref] = [ent]; } else entsByPreferences[pref].push(ent); } if (preferences.length) { preferences.sort((a, b) => a - b); for (let pref of preferences) if (this.RespondToTargetedEntities(entsByPreferences[pref])) return true; } return this.RespondToTargetedEntities(entsWithoutPref); }; /** * Call UnitAI.funcname(args) on all formation members. * @param resetFinishedEntities - If true, call ResetFinishedEntities first. * If the controller wants to wait on its members to finish their order, * this needs to be reset before sending new orders (in case they instafail) * so it makes sense to do it here. * Only set this to false if you're sure it's safe. */ UnitAI.prototype.CallMemberFunction = function(funcname, args, resetFinishedEntities = true) { const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return false; if (resetFinishedEntities) cmpFormation.ResetFinishedEntities(); let result = false; cmpFormation.GetMembers().forEach(ent => { const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI[funcname].apply(cmpUnitAI, args)) result = true; }); return result; }; /** * Call obj.funcname(args) on UnitAI components owned by player in given range. */ UnitAI.prototype.CallPlayerOwnedEntitiesFunctionInRange = function(funcname, args, range) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return; let owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, [owner], IID_UnitAI, true); for (let i = 0; i < nearby.length; ++i) { let cmpUnitAI = Engine.QueryInterface(nearby[i], IID_UnitAI); cmpUnitAI[funcname].apply(cmpUnitAI, args); } }; /** * Call obj.functname(args) on UnitAI components of all formation members, * and return true if all calls return true. */ UnitAI.prototype.TestAllMemberFunction = function(funcname, args) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); return cmpFormation && cmpFormation.GetMembers().every(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI[funcname].apply(cmpUnitAI, args); }); }; UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec); Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Diplomacy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Diplomacy.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Diplomacy.js (revision 27722) @@ -0,0 +1,7 @@ +Engine.RegisterInterface("Diplomacy"); + +/** + * Message of the form { "player": number, "otherPlayer": number } + * sent from Diplomacy component when diplomacy changed for one player or between two players. + */ +Engine.RegisterMessageType("DiplomacyChanged"); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Player.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Player.js (revision 27722) @@ -1,41 +1,35 @@ /** - * Message of the form { "player": number, "otherPlayer": number } - * sent from Player component when diplomacy changed for one player or between two players. - */ -Engine.RegisterMessageType("DiplomacyChanged"); - -/** * Message of the form {} * sent from Player component. */ Engine.RegisterMessageType("DisabledTechnologiesChanged"); /** * Message of the form {} * sent from Player component. */ Engine.RegisterMessageType("DisabledTemplatesChanged"); /** * Message of the form { "playerID": number } * sent from Player component when a player is defeated. */ Engine.RegisterMessageType("PlayerDefeated"); /** * Message of the form { "playerID": number } * sent from Player component when a player has won. */ Engine.RegisterMessageType("PlayerWon"); /** * Message of the form { "to": number, "from": number, "amounts": object } * sent from Player component whenever a tribute is sent. */ Engine.RegisterMessageType("TributeExchanged"); /** * Message of the form { "player": player, "type": "cheat" } * sent from Player when some multiplier of that player has changed */ Engine.RegisterMessageType("MultiplierChanged"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 27722) @@ -1,161 +1,168 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/RangeOverlayManager.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("Auras.js"); Engine.LoadComponentScript("ModifiersManager.js"); var playerID = [0, 1, 2]; var playerEnt = [10, 11, 12]; var playerDefeated = [false, false, false]; var sourceEnt = 20; var targetEnt = 30; var auraRange = 40; var template = { "Identity": { "Classes": { "_string": "CorrectClass OtherClass" } } }; global.AuraTemplates = { "Get": name => { let template = { "type": name, "affectedPlayers": ["Ally"], "affects": ["CorrectClass"], "modifications": [{ "value": "Component/Value", "add": 10 }], "auraName": "name", "auraDescription": "description" }; if (name == "range") template.radius = auraRange; return template; } }; function testAuras(name, test_function) { ResetState(); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": idx => playerEnt[idx], "GetNumPlayers": () => 3, "GetAllPlayers": () => playerID }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "CreateActiveQuery": (ent, minRange, maxRange, players, iid, flags) => 1, "EnableActiveQuery": id => {}, "ResetActiveQuery": id => {}, "DisableActiveQuery": id => {}, "DestroyActiveQuery": id => {}, "GetEntityFlagMask": identifier => {}, "GetEntitiesByPlayer": id => [30, 31, 32] }); AddMock(playerEnt[1], IID_Player, { - "IsAlly": id => id == playerID[1] || id == playerID[2], - "IsEnemy": id => id != playerID[1] || id != playerID[2], "GetPlayerID": () => playerID[1], "IsDefeated": () => playerDefeated[1] }); - AddMock(playerEnt[2], IID_Player, { + AddMock(playerEnt[1], IID_Diplomacy, { "IsAlly": id => id == playerID[1] || id == playerID[2], "IsEnemy": id => id != playerID[1] || id != playerID[2], + }); + + AddMock(playerEnt[2], IID_Player, { "GetPlayerID": () => playerID[2], "IsDefeated": () => playerDefeated[2] }); + AddMock(playerEnt[2], IID_Diplomacy, { + "IsAlly": id => id == playerID[1] || id == playerID[2], + "IsEnemy": id => id != playerID[1] || id != playerID[2], + }); + AddMock(targetEnt, IID_Identity, { "GetClassesList": () => ["CorrectClass", "OtherClass"] }); AddMock(sourceEnt, IID_Position, { "GetPosition2D": () => new Vector2D() }); if (name != "player" || playerEnt.indexOf(targetEnt) == -1) { AddMock(targetEnt, IID_Position, { "GetPosition2D": () => new Vector2D() }); AddMock(targetEnt, IID_Ownership, { "GetOwner": () => playerID[1] }); } if (playerEnt.indexOf(sourceEnt) == -1) AddMock(sourceEnt, IID_Ownership, { "GetOwner": () => playerID[1] }); let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {}); cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": playerID[1], "from": -1, "to": playerEnt[1] }); cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": playerID[2], "from": -1, "to": playerEnt[2] }); let cmpAuras = ConstructComponent(sourceEnt, "Auras", { "_string": name }); test_function(name, cmpAuras); } targetEnt = playerEnt[playerID[2]]; testAuras("player", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); }); targetEnt = 30; // Test the case when the aura source is a player entity. sourceEnt = 11; testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); }); sourceEnt = 20; testAuras("range", (name, cmpAuras) => { cmpAuras.OnRangeUpdate({ "tag": 1, "added": [targetEnt], "removed": [] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 5); cmpAuras.OnRangeUpdate({ "tag": 1, "added": [], "removed": [targetEnt] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("garrisonedUnits", (name, cmpAuras) => { cmpAuras.OnGarrisonedUnitsChanged({ "added": [targetEnt], "removed": [] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.OnGarrisonedUnitsChanged({ "added": [], "removed": [targetEnt] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("garrison", (name, cmpAuras) => { TS_ASSERT_EQUALS(cmpAuras.HasGarrisonAura(), true); cmpAuras.ApplyGarrisonAura(targetEnt); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.RemoveGarrisonAura(targetEnt); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("formation", (name, cmpAuras) => { TS_ASSERT_EQUALS(cmpAuras.HasFormationAura(), true); cmpAuras.ApplyFormationAura([targetEnt]); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.RemoveFormationAura([targetEnt]); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 15); AddMock(sourceEnt, IID_Ownership, { "GetOwner": () => -1 }); cmpAuras.OnOwnershipChanged({ "from": sourceEnt, "to": -1 }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 5); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 5); }); playerDefeated[1] = true; testAuras("global", (name, cmpAuras) => { cmpAuras.OnGlobalPlayerDefeated({ "playerId": playerID[1] }); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 5); }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js (revision 27722) @@ -1,219 +1,220 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Capturable.js"); var testData = { "structure": 20, "playerID": 1, "regenRate": 2, "garrisonedEntities": [30, 31, 32, 33], "garrisonRegenRate": 2, "decay": false, "decayRate": 30, "maxCapturePoints": 3000, "neighbours": [20, 0, 20, 10] }; const testCaptureAttacks = { "30": 1.5, "32": 2.0 }; function testCapturable(testData, test_function) { ResetState(); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": (ent, iid, funcname, time, repeattime, data) => {}, "CancelTimer": timer => {} }); AddMock(testData.structure, IID_Ownership, { "GetOwner": () => testData.playerID, "SetOwner": id => {} }); AddMock(testData.structure, IID_GarrisonHolder, { "GetEntities": () => testData.garrisonedEntities }); AddMock(testData.structure, IID_Fogging, { "Activate": () => {} }); - AddMock(10, IID_Player, { + AddMock(10, IID_Diplomacy, { "IsEnemy": id => id != 0 }); - AddMock(11, IID_Player, { + AddMock(11, IID_Diplomacy, { "IsEnemy": id => id != 1 && id != 2 }); - AddMock(12, IID_Player, { + AddMock(12, IID_Diplomacy, { "IsEnemy": id => id != 1 && id != 2 }); - AddMock(13, IID_Player, { + AddMock(13, IID_Diplomacy, { "IsEnemy": id => id != 3 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": () => 4, "GetPlayerByID": id => 10 + id }); AddMock(testData.structure, IID_StatisticsTracker, { "LostEntity": () => {}, "CapturedBuilding": () => {} }); let cmpCapturable = ConstructComponent(testData.structure, "Capturable", { "CapturePoints": testData.maxCapturePoints, "RegenRate": testData.regenRate, "GarrisonRegenRate": testData.garrisonRegenRate }); AddMock(testData.structure, IID_TerritoryDecay, { "IsDecaying": () => testData.decay, "GetDecayRate": () => testData.decayRate, "GetConnectedNeighbours": () => testData.neighbours }); let regenRate = testData.regenRate; for (const entity of testData.garrisonedEntities) { if (testCaptureAttacks[entity] === undefined) continue; AddMock(entity, IID_Attack, { "GetAttackEffectsData": (type) => { return type === "Capture" ? { "Capture": testCaptureAttacks[entity] } : undefined; }, }); regenRate += testCaptureAttacks[entity] * testData.garrisonRegenRate; } TS_ASSERT_EQUALS(cmpCapturable.GetRegenRate(), regenRate); test_function(cmpCapturable); Engine.PostMessage = (ent, iid, message) => {}; } // Tests initialisation of the capture points when the entity is created testCapturable(testData, cmpCapturable => { Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS(message, { "regenerating": true, "regenRate": cmpCapturable.GetRegenRate(), "territoryDecay": 0 }); }; cmpCapturable.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": testData.playerID }); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 3000, 0, 0]); }); // Tests if the message is sent when capture points change testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 2000, 0, 1000]); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS(message, { "capturePoints": [0, 2000, 0, 1000] }); }; cmpCapturable.RegisterCapturePointsChanged(); }); // Tests reducing capture points (after a capture attack or a decay) testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CapturePointsChanged) TS_ASSERT_UNEVAL_EQUALS(message, { "capturePoints": [0, 2000 - 100, 0, 1000 + 100] }); if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message, { "regenerating": true, "regenRate": cmpCapturable.GetRegenRate(), "territoryDecay": 0 }); }; TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.Reduce(100, 3), 100); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 2000 - 100, 0, 1000 + 100]); }); // Tests reducing capture points (after a capture attack or a decay) testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); TS_ASSERT_EQUALS(cmpCapturable.Reduce(2500, 3), 2000); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 0, 0, 3000]); }); function testRegen(testData, capturePointsIn, capturePointsOut, regenerating) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints(capturePointsIn); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message.regenerating, regenerating); }; cmpCapturable.TimerTick(capturePointsIn); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), capturePointsOut); }); } // With our testData, the total regen rate is 9. That should be taken from the enemies testRegen(testData, [12, 2950, 2, 36], [7.5, 2959, 2, 31.5], true); testRegen(testData, [0, 2994, 2, 4], [0, 2998, 2, 0], true); testRegen(testData, [0, 2998, 2, 0], [0, 2998, 2, 0], false); // If the regeneration rate becomes negative, capture points are given in favour of gaia testData.regenRate = -19; // With our testData, the total regen rate is -12. That should be taken from all players to gaia testRegen(testData, [100, 2800, 50, 50], [112, 2796, 46, 46], true); testData.regenRate = 2; function testDecay(testData, capturePointsIn, capturePointsOut) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints(capturePointsIn); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message.territoryDecay, testData.decayRate); }; cmpCapturable.TimerTick(); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), capturePointsOut); }); } testData.decay = true; testData.regenRate = 8; testData.garrisonRegenRate = 4; // With our testData, the decay rate is 30, that should be given to all neighbours with weights [20/50, 0, 20/50, 10/50], then it regens. testDecay(testData, [2900, 35, 10, 55], [2901, 27, 22, 50]); testData.decay = false; // Tests Reduce function testReduce(testData, amount, player, taken) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.Reduce(amount, player), taken); }); } testReduce(testData, 50, 3, 50); testReduce(testData, 50, 2, 50); testReduce(testData, 50, 1, 50); testReduce(testData, -50, 3, 0); testReduce(testData, 50, 0, 50); testReduce(testData, 0, 3, 0); testReduce(testData, 1500, 3, 1500); testReduce(testData, 2000, 3, 2000); testReduce(testData, 3000, 3, 2000); // Test defeated player testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([500, 1000, 0, 250]); cmpCapturable.OnGlobalPlayerDefeated({ "playerId": 3 }); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [750, 1000, 0, 0]); }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Diplomacy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Diplomacy.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Diplomacy.js (revision 27722) @@ -0,0 +1,79 @@ +Engine.LoadComponentScript("interfaces/Diplomacy.js"); +Engine.LoadComponentScript("interfaces/PlayerManager.js"); +Engine.LoadComponentScript("Diplomacy.js"); +Engine.LoadHelperScript("Player.js"); + +const players = [10, 11]; + +ConstructComponent(players[0], "Diplomacy", null) +const cmpDiplomacy = ConstructComponent(players[1], "Diplomacy", {}) +TS_ASSERT_EQUALS(cmpDiplomacy.GetTeam(), -1); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetAllies(), []); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetEnemies(), []); + +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetNumPlayers": () => players.length, + "GetPlayerByID": (i) => players[i] +}); + +for (const player in players) + AddMock(players[player], IID_Player, { + "GetPlayerID": () => player, + "IsActive": () => true, + }); + +cmpDiplomacy.ChangeTeam(1); +TS_ASSERT_EQUALS(cmpDiplomacy.GetTeam(), 1); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetAllies(), [1]); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetEnemies(), []); + +cmpDiplomacy.LockTeam(); +cmpDiplomacy.ChangeTeam(2); +TS_ASSERT_EQUALS(cmpDiplomacy.GetTeam(), 1); + +cmpDiplomacy.UnLockTeam(); +cmpDiplomacy.SetEnemy(0); + +TS_ASSERT(!cmpDiplomacy.IsAlly(0)); +TS_ASSERT(!cmpDiplomacy.IsNeutral(0)); +TS_ASSERT(cmpDiplomacy.IsEnemy(0)); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetAllies(), [1]); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetEnemies(), [0]); + +cmpDiplomacy.Ally(0); + +TS_ASSERT(cmpDiplomacy.IsAlly(0)); +TS_ASSERT(!cmpDiplomacy.IsNeutral(0)); +TS_ASSERT(!cmpDiplomacy.IsEnemy(0)); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetAllies(), [0, 1]); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetEnemies(), []); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetMutualAllies(), [1]); + +cmpDiplomacy.SetNeutral(0); + +TS_ASSERT(!cmpDiplomacy.IsAlly(0)); +TS_ASSERT(cmpDiplomacy.IsNeutral(0)); +TS_ASSERT(!cmpDiplomacy.IsEnemy(0)); + +// Mutual worsening of relations. +cmpDiplomacy.OnDiplomacyChanged({ + "player": 0, + "otherPlayer": 1, + "value": -1 +}); +TS_ASSERT(cmpDiplomacy.IsEnemy(0)); + + +cmpDiplomacy.SetDiplomacy([-1, 1, 0, 1, -1]); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetAllies(), [1, 3]); +TS_ASSERT_UNEVAL_EQUALS(cmpDiplomacy.GetEnemies(), [0, 4]); + +// Check diplomacy is not editable outside of the component. +var diplo = cmpDiplomacy.GetDiplomacy(); +diplo[0] = 1; +TS_ASSERT(cmpDiplomacy.IsEnemy(0)); + +diplo = [1, 1, 0]; +cmpDiplomacy.SetDiplomacy(diplo); +diplo[1] = -1; +TS_ASSERT(cmpDiplomacy.IsAlly(1)); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js (revision 27722) @@ -1,142 +1,141 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Position.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Garrisonable.js"); Engine.LoadComponentScript("GarrisonHolder.js"); const player = 1; const enemyPlayer = 2; const friendlyPlayer = 3; const garrison = 10; const holder = 11; let createGarrisonCmp = entity => { AddMock(entity, IID_Identity, { "GetClassesList": () => ["Ranged"], "GetSelectionGroupName": () => "mace_infantry_archer_a" }); AddMock(entity, IID_Ownership, { "GetOwner": () => player }); AddMock(entity, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {}, "SetYRotation": angle => {} }); return ConstructComponent(entity, "Garrisonable", { "Size": "1" }); }; AddMock(holder, IID_Footprint, { "PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30), "PickSpawnPoint": entity => new Vector3D(4, 3, 30) }); AddMock(holder, IID_Ownership, { "GetOwner": () => player }); -AddMock(player, IID_Player, { +AddMock(player, IID_Diplomacy, { "IsAlly": id => id != enemyPlayer, "IsMutualAlly": id => id != enemyPlayer, - "GetPlayerID": () => player }); -AddMock(friendlyPlayer, IID_Player, { +AddMock(friendlyPlayer, IID_Diplomacy, { "IsAlly": id => true, "IsMutualAlly": id => true, - "GetPlayerID": () => friendlyPlayer }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": (ent, iid, funcname, time, data) => 1 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id }); AddMock(garrison, IID_Identity, { "GetClassesList": () => ["Ranged"], "GetSelectionGroupName": () => "mace_infantry_archer_a" }); AddMock(garrison, IID_Ownership, { "GetOwner": () => player }); AddMock(garrison, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {}, "SetYRotation": angle => {} }); let cmpGarrisonable = ConstructComponent(garrison, "Garrisonable", { "Size": "1" }); let cmpGarrisonHolder = ConstructComponent(holder, "GarrisonHolder", { "Max": "10", "List": { "_string": "Ranged" }, "EjectHealth": "0.1", "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": "1", "LoadingRange": "2.1", "Pickup": "false" }); TS_ASSERT(cmpGarrisonable.Garrison(holder)); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]); cmpGarrisonable.OnEntityRenamed({ "entity": garrison, "newentity": -1 }); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); TS_ASSERT(cmpGarrisonable.Garrison(holder)); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]); // Can't garrison twice. TS_ASSERT(!cmpGarrisonable.Garrison(holder)); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]); TS_ASSERT(cmpGarrisonHolder.Unload(garrison)); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); // Test initGarrison. let entities = [21, 22, 23, 24]; for (let entity of entities) createGarrisonCmp(entity); cmpGarrisonHolder.SetInitGarrison(entities); cmpGarrisonHolder.OnGlobalInitGame(); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), entities); // They turned against us! AddMock(entities[0], IID_Ownership, { "GetOwner": () => enemyPlayer }); cmpGarrisonHolder.OnDiplomacyChanged(); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), entities.length - 1); TS_ASSERT(cmpGarrisonHolder.UnloadAll()); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Heal.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Heal.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Heal.js (revision 27722) @@ -1,224 +1,225 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Heal.js"); Engine.LoadComponentScript("Timer.js"); const entity = 60; const player = 1; const otherPlayer = 2; AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": () => true }); let template = { "Range": "20", "RangeOverlay": { "LineTexture": "heal_overlay_range.png", "LineTextureMask": "heal_overlay_range_mask.png", "LineThickness": "0.35" }, "Health": "5", "Interval": "2000", "UnhealableClasses": { "_string": "Cavalry" }, "HealableClasses": { "_string": "Support Infantry" } }; AddMock(entity, IID_Ownership, { "GetOwner": () => player }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => player }); -AddMock(player, IID_Player, { +AddMock(player, IID_Diplomacy, { "IsAlly": (p) => p == player }); -AddMock(otherPlayer, IID_Player, { +AddMock(otherPlayer, IID_Diplomacy, { "IsAlly": (p) => p == player }); ApplyValueModificationsToEntity = function(value, stat, ent) { if (ent != entity) return stat; switch (value) { case "Heal/Health": return stat + 100; case "Heal/Interval": return stat + 200; case "Heal/Range": return stat + 300; default: return stat; } }; let cmpHeal = ConstructComponent(60, "Heal", template); // Test Getters TS_ASSERT_EQUALS(cmpHeal.GetInterval(), 2000 + 200); TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetTimers(), { "prepare": 1000, "repeat": 2000 + 200 }); TS_ASSERT_EQUALS(cmpHeal.GetHealth(), 5 + 100); TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetRange(), { "min": 0, "max": 20 + 300 }); TS_ASSERT_EQUALS(cmpHeal.GetHealableClasses(), "Support Infantry"); TS_ASSERT_EQUALS(cmpHeal.GetUnhealableClasses(), "Cavalry"); TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetRangeOverlays(), [{ "radius": 20 + 300, "texture": "heal_overlay_range.png", "textureMask": "heal_overlay_range_mask.png", "thickness": 0.35 }]); // Test healing. let target = 70; AddMock(target, IID_Ownership, { "GetOwner": () => player }); let targetClasses = ["Infantry"]; AddMock(target, IID_Identity, { "GetClassesList": () => targetClasses }); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); let increased; let unhealable = false; AddMock(target, IID_Health, { "GetMaxHitpoints": () => 700, "Increase": amount => { increased = true; TS_ASSERT_EQUALS(amount, 5 + 100); return { "old": 600, "new": 600 + 5 + 100 }; }, "IsUnhealable": () => unhealable, "IsInjured": () => true }); TS_ASSERT(cmpHeal.StartHealing(target)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(increased); increased = false; cmpTimer.OnUpdate({ "turnLength": 2.2 }); TS_ASSERT(increased); // Test we can't heal too quickly. increased = false; TS_ASSERT(cmpHeal.StartHealing(target)); cmpTimer.OnUpdate({ "turnLength": 2 }); TS_ASSERT(!increased); // Test experience. let looted; AddMock(target, IID_Loot, { "GetXp": () => { looted = true; return 80; } }); let promoted; AddMock(entity, IID_Promotion, { "IncreaseXp": amount => { promoted = true; TS_ASSERT_EQUALS(amount, (5 + 100) * 80 / 700); } }); increased = false; TS_ASSERT(cmpHeal.StartHealing(target)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(increased && looted && promoted); // Test OnValueModification let updated; AddMock(entity, IID_UnitAI, { "FaceTowardsTarget": () => {}, "UpdateRangeQueries": () => { updated = true; } }); cmpHeal.OnValueModification({ "component": "Heal", "valueNames": ["Heal/Health"] }); TS_ASSERT(!updated); cmpHeal.OnValueModification({ "component": "Heal", "valueNames": ["Heal/Range"] }); TS_ASSERT(updated); // Test CanHeal. targetClasses = ["Infantry", "Hero"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), true); targetClasses = ["Hero"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), false); targetClasses = ["Infantry", "Cavalry"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), false); targetClasses = ["Cavalry"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), false); targetClasses = ["Infantry"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), true); unhealable = true; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), false); let otherTarget = 71; AddMock(otherTarget, IID_Ownership, { "GetOwner": () => otherPlayer }); AddMock(otherTarget, IID_Health, { "IsUnhealable": () => false, "IsInjured": () => true }); AddMock(otherTarget, IID_Identity, { "GetClassesList": () => ["Infantry"] }); TS_ASSERT(!cmpHeal.CanHeal(otherTarget)); // Test we stop healing when finished. increased = false; AddMock(target, IID_Health, { "GetMaxHitpoints": () => 700, "Increase": amount => { increased = true; TS_ASSERT_EQUALS(amount, 5 + 100); return { "old": 600, "new": 600 + 5 + 100 }; }, "IsUnhealable": () => false, "IsInjured": () => true }); TS_ASSERT(cmpHeal.StartHealing(target)); cmpTimer.OnUpdate({ "turnLength": 2.2 }); TS_ASSERT(increased); increased = false; AddMock(target, IID_Health, { "GetMaxHitpoints": () => 700, "Increase": amount => { increased = true; TS_ASSERT(false); }, "IsUnhealable": () => false, "IsInjured": () => false }); cmpTimer.OnUpdate({ "turnLength": 2.2 }); TS_ASSERT(!increased); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Turrets.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Turrets.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Turrets.js (revision 27722) @@ -1,114 +1,113 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Position.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Turretable.js"); Engine.LoadComponentScript("interfaces/TurretHolder.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Turretable.js"); Engine.LoadComponentScript("TurretHolder.js"); const player = 1; const enemyPlayer = 2; const friendlyPlayer = 3; const turret = 10; const holder = 11; let createTurretCmp = entity => { AddMock(entity, IID_Identity, { "GetClassesList": () => ["Ranged"], "GetSelectionGroupName": () => "mace_infantry_archer_a" }); AddMock(entity, IID_Ownership, { "GetOwner": () => player }); AddMock(entity, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {}, "SetTurretParent": entity => {}, "SetYRotation": angle => {} }); return ConstructComponent(entity, "Turretable", null); }; AddMock(holder, IID_Footprint, { "PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30), "PickSpawnPoint": entity => new Vector3D(4, 3, 30) }); AddMock(holder, IID_Ownership, { "GetOwner": () => player }); -AddMock(player, IID_Player, { +AddMock(player, IID_Diplomacy, { "IsAlly": id => id != enemyPlayer, "IsMutualAlly": id => id != enemyPlayer, - "GetPlayerID": () => player }); -AddMock(friendlyPlayer, IID_Player, { +AddMock(friendlyPlayer, IID_Diplomacy, { "IsAlly": id => true, "IsMutualAlly": id => true, - "GetPlayerID": () => friendlyPlayer }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id }); AddMock(holder, IID_Position, { "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6) }); let cmpTurretable = createTurretCmp(turret); let cmpTurretHolder = ConstructComponent(holder, "TurretHolder", { "TurretPoints": { "archer1": { "X": "12.0", "Y": "5.", "Z": "6.0" }, "archer2": { "X": "15.0", "Y": "5.0", "Z": "6.0" } } }); TS_ASSERT(cmpTurretable.OccupyTurret(holder)); TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), [turret]); TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(turret)); TS_ASSERT(cmpTurretable.LeaveTurret()); TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), []); // Test renaming on a turret. // Ensure we test renaming from the second spot, not the first. const newTurret = 31; let cmpTurretableNew = createTurretCmp(newTurret); TS_ASSERT(cmpTurretableNew.OccupyTurret(holder)); TS_ASSERT(cmpTurretable.OccupyTurret(holder)); TS_ASSERT(cmpTurretableNew.LeaveTurret()); let previousTurret = cmpTurretHolder.GetOccupiedTurretPointName(turret); cmpTurretable.OnEntityRenamed({ "entity": turret, "newentity": newTurret }); let newTurretPos = cmpTurretHolder.GetOccupiedTurretPointName(newTurret); TS_ASSERT_UNEVAL_EQUALS(newTurretPos, previousTurret); TS_ASSERT(cmpTurretableNew.LeaveTurret()); // Test initTurrets. cmpTurretHolder.SetInitEntity("archer1", turret); cmpTurretHolder.SetInitEntity("archer2", newTurret); cmpTurretHolder.OnGlobalInitGame(); TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), [turret, newTurret]); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 27722) @@ -1,1836 +1,1837 @@ // 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) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; let data = { "cmpPlayer": cmpPlayer, "controlAllUnits": cmpPlayer.CanControlAllUnits() }; if (cmd.entities) data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits); // TODO: queuing order and forcing formations doesn't really work. // To play nice, we'll still no-formation queued order if units are in formation // but the opposite perhaps ought to be implemented. if (!cmd.queued || cmd.formation == NULL_FORMATION) data.formation = cmd.formation || undefined; // Allow focusing the camera on recent commands let commandData = { "type": "playercommand", "players": [player], "cmd": cmd }; // Save the position, since the GUI event is received after the unit died if (cmd.type == "delete-entities") { let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position); commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D(); } let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification(commandData); // 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 (g_Commands[cmd.type]) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("OnPlayerCommand", { "player": player, "cmd": cmd }); g_Commands[cmd.type](player, cmd, data); } else error("Invalid command: unknown command type: "+uneval(cmd)); } var g_Commands = { "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); }, "collect-treasure": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CollectTreasure(cmd.target, cmd.queued); }); }, "collect-treasure-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, cmd.queued); }); }, "diplomacy": function(player, cmd, data) { - let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); - if (data.cmpPlayer.GetLockTeams() || - cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive()) + if (Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive()) + return; + + const cmpDiplomacy = QueryPlayerIDInterface(player, IID_Diplomacy); + if (!cmpDiplomacy || cmpDiplomacy.IsTeamLocked()) return; switch(cmd.to) { case "ally": - data.cmpPlayer.SetAlly(cmd.player); + cmpDiplomacy.Ally(cmd.player); break; case "neutral": - data.cmpPlayer.SetNeutral(cmd.player); + cmpDiplomacy.SetNeutral(cmd.player); break; case "enemy": - data.cmpPlayer.SetEnemy(cmd.player); + cmpDiplomacy.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({ + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "diplomacy", "players": [player], "targetPlayer": cmd.player, "status": cmd.to }); }, "tribute": function(player, cmd, data) { data.cmpPlayer.TributeResource(cmd.player, cmd.amounts); }, "control-all": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - control all units)") }); data.cmpPlayer.SetControlAllUnits(cmd.flag); }, "reveal-map": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - reveal map)") }); // 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, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront); }); }, "walk-custom": function(player, cmd, data) { for (let ent in data.entities) GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued, cmd.pushFront); }); }, "walk-to-range": function(player, cmd, data) { // Only used by the AI for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued, cmd.pushFront); } }, "attack-walk": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront); }); }, "attack-walk-custom": function(player, cmd, data) { for (let ent in data.entities) GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront); }); }, "attack": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Attack(cmd.target, cmd.allowCapture, cmd.queued, cmd.pushFront); }); }, "patrol": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued) ); }, "heal": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Heal(cmd.target, cmd.queued, cmd.pushFront); }); }, "repair": function(player, cmd, data) { // This covers both repairing damaged buildings, and constructing unfinished foundations if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued, cmd.pushFront); }); }, "gather": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Gather(cmd.target, cmd.queued, cmd.pushFront); }); }, "gather-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued, cmd.pushFront); }); }, "returnresource": function(player, cmd, data) { if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.ReturnResource(cmd.target, cmd.queued, cmd.pushFront); }); }, "back-to-work": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(!cmpUnitAI || !cmpUnitAI.BackToWork()) notifyBackToWorkFailure(player); } }, "call-to-arms": function(player, cmd, data) { const unitsToMove = data.entities.filter(ent => MatchesClassList(Engine.QueryInterface(ent, IID_Identity).GetClassesList(), ["Soldier", "Warship", "Siege", "Healer"]) ); GetFormationUnitAIs(unitsToMove, player, cmd, data.formation).forEach(cmpUnitAI => { if (cmd.pushFront) { cmpUnitAI.WalkAndFight(cmd.position.x, cmd.position.z, cmd.targetClasses, cmd.allowCapture, false, cmd.pushFront); cmpUnitAI.DropAtNearestDropSite(false, cmd.pushFront); } else { cmpUnitAI.DropAtNearestDropSite(cmd.queued, false) cmpUnitAI.WalkAndFight(cmd.position.x, cmd.position.z, cmd.targetClasses, cmd.allowCapture, true, false); } }); }, "remove-guard": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.RemoveGuard(); } }, "train": function(player, cmd, data) { if (!Number.isInteger(cmd.count) || cmd.count <= 0) { warn("Invalid command: can't train " + uneval(cmd.count) + " units"); return; } // Check entity limits var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).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 (let ent of data.entities) { if (unitCategory) { var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits); if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit)) { if (g_DebugCommands) warn(unitCategory + " train limit is reached: " + uneval(cmd)); continue; } } var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: training requires unresearched technology: " + uneval(cmd)); continue; } const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer); if (!cmpTrainer) continue; let templateName = cmd.template; // 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 (data.cmpPlayer.IsAI()) templateName = GetUpgradedTemplate(player, cmd.template); if (cmpTrainer.CanTrain(templateName)) Engine.QueryInterface(ent, IID_ProductionQueue)?.AddItem(templateName, "unit", +cmd.count, cmd.metadata, cmd.pushFront); } }, "research": function(player, cmd, data) { var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager); if (cmpTechnologyManager && !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.AddItem(cmd.template, "technology", undefined, cmd.metadata, cmd.pushFront); }, "stop-production": function(player, cmd, data) { let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.RemoveItem(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 (let ent of data.entities) { if (!data.controlAllUnits) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity && cmpIdentity.IsUndeletable()) continue; let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable && cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2) continue; let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather()) continue; } let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) { let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health); if (cmpMiragedHealth) cmpMiragedHealth.Kill(); else Engine.DestroyEntity(cmpMirage.parent); Engine.DestroyEntity(ent); continue; } let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(ent); } }, "set-rallypoint": function(player, cmd, data) { for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { if (!cmd.queued) cmpRallyPoint.Unset(); cmpRallyPoint.AddPosition(cmd.x, cmd.z); cmpRallyPoint.AddData(clone(cmd.data)); } } }, "unset-rallypoint": function(player, cmd, data) { for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Reset(); } }, "resign": function(player, cmd, data) { data.cmpPlayer.Defeat(markForTranslation("%(player)s has resigned.")); }, "occupy-turret": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.OccupyTurret(cmd.target, cmd.queued); }); }, "garrison": function(player, cmd, data) { if (!CanPlayerOrAllyControlUnit(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, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Garrison(cmd.target, cmd.queued, cmd.pushFront); }); }, "guard": function(player, cmd, data) { if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Guard(cmd.target, cmd.queued, cmd.pushFront); }); }, "stop": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Stop(cmd.queued); }); }, "leave-turret": function(player, cmd, data) { let notUnloaded = 0; for (let ent of data.entities) { let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (!cmpTurretable || !cmpTurretable.LeaveTurret()) ++notUnloaded; } if (notUnloaded) notifyUnloadFailure(player); }, "unload-turrets": function(player, cmd, data) { let notUnloaded = 0; for (let ent of data.entities) { let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); for (let turret of cmpTurretHolder.GetEntities()) { let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable); if (!cmpTurretable || !cmpTurretable.LeaveTurret()) ++notUnloaded; } } if (notUnloaded) notifyUnloadFailure(player); }, "unload": function(player, cmd, data) { if (!CanPlayerOrAllyControlUnit(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 (let ent of data.entities) if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent)) ++notUngarrisoned; if (notUngarrisoned != 0) notifyUnloadFailure(player, cmd.garrisonHolder); }, "unload-template": function(player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of 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.owner) continue; if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all)) notifyUnloadFailure(player, garrisonHolder); } } }, "unload-all-by-owner": function(player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player)) notifyUnloadFailure(player, garrisonHolder); } }, "unload-all": function(player, cmd, data) { var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll()) notifyUnloadFailure(player, garrisonHolder); } }, "alert-raise": function(player, cmd, data) { for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.RaiseAlert(); } }, "alert-end": function(player, cmd, data) { for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.EndOfAlert(); } }, "formation": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => { cmpUnitAI.MoveIntoFormation(cmd); }); }, "promote": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - promoted units)"), "translateMessage": true }); for (let ent of cmd.entities) { var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp()); } }, "stance": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && !cmpUnitAI.IsTurret()) cmpUnitAI.SwitchToStance(cmd.name); } }, "lock-gate": function(player, cmd, data) { for (let ent of data.entities) { var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (!cmpGate) continue; if (cmd.lock) cmpGate.LockGate(); else cmpGate.UnlockGate(); } }, "setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); }); }, "cancel-setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CancelSetupTradeRoute(cmd.target); }); }, "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(player, cmd.sell, cmd.buy, cmd.amount); }, "set-shading-color": function(player, cmd, data) { // Prevent multiplayer abuse if (!data.cmpPlayer.IsAI()) return; // Debug command to make an entity brightly colored for (let ent of 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 (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; if (cmd.pack) cmpUnitAI.Pack(cmd.queued, cmd.pushFront); else cmpUnitAI.Unpack(cmd.queued, cmd.pushFront); } }, "cancel-pack": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; if (cmd.pack) cmpUnitAI.CancelPack(cmd.queued, cmd.pushFront); else cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront); } }, "upgrade": function(player, cmd, data) { for (let ent of data.entities) { var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template)) continue; if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template)) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [player], "message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.") }); continue; } // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template)) { if (g_DebugCommands) warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd)); continue; } if (!RequirementsHelper.AreRequirementsMet(cmpUpgrade.GetRequirements(cmd.template), player)) { if (g_DebugCommands) warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd)); continue; } cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer); } }, "cancel-upgrade": function(player, cmd, data) { for (let ent of data.entities) { let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) cmpUpgrade.CancelUpgrade(player); } }, "attack-request": function(player, cmd, data) { // Send a chat message to human players var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": "/allies " + markForTranslation("Attack against %(_player_)s requested."), "translateParameters": ["_player_"], "parameters": { "_player_": cmd.player } }); // And send an attackRequest event to the AIs let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("AttackRequest", cmd); }, "spy-request": function(player, cmd, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => { let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing); return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player); })); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "spy-response", "players": [player], "target": cmd.player, "entity": ent }); if (ent) Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source); else { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy"); IncurBribeCost(template, player, cmd.player, true); // update statistics for failed bribes let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpBribesStatisticsTracker) cmpBribesStatisticsTracker.IncreaseFailedBribesCounter(); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("There are no bribable units"), "translateMessage": true }); } }, "diplomacy-request": function(player, cmd, data) { let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("DiplomacyRequest", cmd); }, "tribute-request": function(player, cmd, data) { let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("TributeRequest", cmd); }, "dialog-answer": function(player, cmd, data) { // Currently nothing. Triggers can read it anyway, and send this // message to any component you like. }, "set-dropsite-sharing": function(player, cmd, data) { for (let ent of data.entities) { let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite && cmpResourceDropsite.IsSharable()) cmpResourceDropsite.SetSharing(cmd.shared); } }, "map-flare": function(player, cmd, data) { let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "map-flare", "players": [player], "position": cmd.position }); }, "autoqueue-on": function(player, cmd, data) { for (let ent of data.entities) { let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.EnableAutoQueue(); } }, "autoqueue-off": function(player, cmd, data) { for (let ent of data.entities) { let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.DisableAutoQueue(); } }, }; /** * Sends a GUI notification about unit(s) that failed to ungarrison. */ function notifyUnloadFailure(player) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("Unable to unload unit(s)."), "translateMessage": true }); } /** * Sends a GUI notification about worker(s) that failed to go back to work. */ function notifyBackToWorkFailure(player) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("Some unit(s) can't go back to work"), "translateMessage": true }); } /** * Sends a GUI notification about entities that can't be controlled. * @param {number} player - The player-ID of the player that needs to receive this message. */ function notifyOrderFailure(entity, player) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); if (!cmpIdentity) return; let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("%(unit)s can't be controlled."), "parameters": { "unit": cmpIdentity.GetGenericName() }, "translateParameters": ["unit"], "translateMessage": true }); } /** * Get some information about the formations used by entities. */ function ExtractFormations(ents) { let entities = []; // Entities with UnitAI. let members = {}; // { formationentity: [ent, ent, ...], ... } let templates = {}; // { formationentity: template } for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; entities.push(ent); let fid = cmpUnitAI.GetFormationController(); if (fid == INVALID_ENTITY) continue; if (!members[fid]) { members[fid] = []; templates[fid] = cmpUnitAI.GetFormationTemplate(); } members[fid].push(ent); } return { "entities": entities, "members": members, "templates": templates }; } /** * 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 (let 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 template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var angle = cmd.angle; if (template.BuildRestrictions.PlacementType === "shore") { let angleDock = GetDockAngle(template, cmd.x, cmd.z); if (angleDock !== undefined) angle = angleDock; } // Move the foundation to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(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 && !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({ "type": "text", "players": [player], "message": markForTranslation("The building's technology requirements are not met."), "translateMessage": true }); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); } // We need the cost after tech and aura modifications. let cmpCost = Engine.QueryInterface(ent, IID_Cost); let costs = cmpCost.GetResourceCosts(); 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(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, "pushFront": cmd.pushFront, "formation": cmd.formation || undefined }); } 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; var queued = cmd.queued; var pieces = clone(cmd.pieces); for (; i < pieces.length; ++i) { var piece = pieces[i]; // All wall pieces after the first must be queued. if (i > 0 && !queued) 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": 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 == 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(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 break; } var lastBuiltPieceIndex = i - 1; var wallComplete = (lastBuiltPieceIndex == 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 = 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) { let formation = ExtractFormations(ents); for (let fid in formation.members) { let 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, cmd, formationTemplate, forceTemplate) { // If an individual was selected, remove it from any formation // and command it individually. if (ents.length == 1) { let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); if (!cmpUnitAI) return []; RemoveFromFormation(ents); return [ cmpUnitAI ]; } let formationUnitAIs = []; // Find what formations the selected entities are currently in, // and default to that unless the formation is forced or it's the null formation // (we want that to reset whatever formations units are in). if (formationTemplate != NULL_FORMATION) { let formation = ExtractFormations(ents); let formationIds = Object.keys(formation.members); if (formationIds.length == 1) { // Selected units either belong to this formation or have no formation. let fid = formationIds[0]; let 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. if (!forceTemplate || formationTemplate == formation.templates[fid]) { formationTemplate = formation.templates[fid]; formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; } else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate)) formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)]; } else if (cmpFormation && !forceTemplate) { // Just reuse the template. formationTemplate = formation.templates[fid]; } } else if (formationIds.length) { // Check if all entities share a common formation, if so reuse this template. let template = formation.templates[formationIds[0]]; for (let i = 1; i < formationIds.length; ++i) if (formation.templates[formationIds[i]] != template) { template = null; break; } if (template && !forceTemplate) formationTemplate = template; } } // Separate out the units that don't support the chosen formation. let formedUnits = []; let nonformedUnitAIs = []; for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) continue; // 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. let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION; if (nullFormation || !cmpUnitAI.CanUseFormation(formationTemplate || NULL_FORMATION)) { if (nullFormation && cmpUnitAI.GetFormationController()) cmpUnitAI.LeaveFormation(cmd.queued || false); nonformedUnitAIs.push(cmpUnitAI); } else formedUnits.push(ent); } if (nonformedUnitAIs.length == ents.length) { // No units support the formation. return nonformedUnitAIs; } if (!formationUnitAIs.length) { // We need to give the selected units a new formation controller. // TODO replace the fixed 60 with something sensible, based on vision range f.e. let formationSeparation = 60; let clusters = ClusterEntities(formedUnits, formationSeparation); let formationEnts = []; for (let cluster of clusters) { RemoveFromFormation(cluster); if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) { for (let ent of cluster) nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI)); continue; } // Create the new controller. let formationEnt = Engine.AddEntity(formationTemplate); let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); cmpFormation.SetFormationSeparation(formationSeparation); cmpFormation.SetMembers(cluster); for (let ent of formationEnts) cmpFormation.RegisterTwinFormation(ent); formationEnts.push(formationEnt); let 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) { let clusters = []; if (!ents.length) return clusters; let distSq = separationDistance * separationDistance; let positions = []; // triangular matrix with the (squared) distances between the different clusters // the other half is not initialised let matrix = []; for (let i = 0; i < ents.length; ++i) { matrix[i] = []; clusters.push([ents[i]]); let cmpPosition = Engine.QueryInterface(ents[i], IID_Position); positions.push(cmpPosition.GetPosition2D()); for (let 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 let closeClusters = undefined; for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i) for (let 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 let 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. let distances = []; for (let i = 0; i < clusters.length; ++i) { let a = closeClusters[1]; let b = closeClusters[0]; if (i == a || i == b) continue; let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a]; let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b]; 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 (let 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 template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).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 const requirements = GetFormationRequirements(formationTemplate); if (!requirements) return false; let count = 0; for (const ent of ents) if (Engine.QueryInterface(ent, IID_UnitAI)?.CanUseFormation(formationTemplate)) ++count; return count >= requirements.minCount; } /** * Check if player can control this entity * returns: true if the entity is owned by the player and controllable * or control all units is activated, else false */ function CanControlUnit(entity, player, controlAll) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); let canBeControlled = IsOwnedByPlayer(player, entity) && (!cmpIdentity || cmpIdentity.IsControllable()) || controlAll; if (!canBeControlled) notifyOrderFailure(entity, player); return canBeControlled; } /** * @param {number} entity - The entityID to verify. * @param {number} player - The playerID to check against. * @return {boolean}. */ function IsOwnedByPlayerOrMutualAlly(entity, player) { return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity); } /** * Check if player can control this entity * @return {boolean} - True if the entity is valid and controlled by the player * or the entity is owned by an mutualAlly and can be controlled * or control all units is activated, else false. */ function CanPlayerOrAllyControlUnit(entity, player, controlAll) { return CanControlUnit(player, entity, controlAll) || IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity); } /** * @return {boolean} - Whether the owner of this entity can control the entity. */ function CanOwnerControlEntity(entity) { let cmpOwner = QueryOwnerInterface(entity); return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID()); } /** * Filter entities which the player can control. */ function FilterEntityList(entities, player, controlAll) { return entities.filter(ent => CanControlUnit(ent, player, controlAll)); } /** * Filter entities which the player can control or are mutualAlly */ function FilterEntityListWithAllies(entities, player, controlAll) { return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll)); } /** * Incur the player with the cost of a bribe, optionally multiply the cost with * the additionalMultiplier */ function IncurBribeCost(template, player, playerBribed, failedBribe) { let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed); if (!cmpPlayerBribed) return false; let costs = {}; // Additional cost for this owner let multiplier = cmpPlayerBribed.GetSpyCostMultiplier(); if (failedBribe) multiplier *= template.VisionSharing.FailureCostRatio; for (let res in template.Cost.Resources) costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template)); let cmpPlayer = QueryPlayerIDInterface(player); return cmpPlayer && cmpPlayer.TrySubtractResources(costs); } Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements); Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("GetDockAngle", GetDockAngle); Engine.RegisterGlobal("ProcessCommand", ProcessCommand); Engine.RegisterGlobal("g_Commands", g_Commands); Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost); Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_player.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_player.xml (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_player.xml (revision 27722) @@ -1,133 +1,135 @@ 80 160 60000 200 12 0.04 4 0 8 + + unlock_shared_los + unlock_shared_dropsites + 50 1 0 1 1 1 1 5 2 0 2 1 1 1 5 20 8 1 15 4 5 phase_town Player true 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 - unlock_shared_los - unlock_shared_dropsites 1.0 special/formations/null special/formations/box special/formations/column_closed special/formations/line_closed special/formations/column_open special/formations/line_open special/formations/flank special/formations/battle_line special/formations/skirmish special/formations/wedge 0.0 0.0 0.0 0.0 1000 interface/alarm/alarm_defeated.xml interface/alarm/alarm_defeated_ally.xml interface/alarm/alarm_defeated_enemy.xml interface/alarm/alarm_no_idle_unit.xml Cavalry Champion Domestic FemaleCitizen Hero Infantry Ship Siege Trader Unit Worker Economic CivCentre Fortress House Military Outpost Structure Wonder Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 27722) @@ -1,627 +1,634 @@ Engine.LoadHelperScript("ObstructionSnap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/AlertRaiser.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Barter.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/CeasefireManager.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Gate.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Market.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/Population.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/Researcher.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/ResourceDropsite.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/ResourceTrickle.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Trader.js"); Engine.LoadComponentScript("interfaces/Trainer.js"); Engine.LoadComponentScript("interfaces/TurretHolder.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/Treasure.js"); Engine.LoadComponentScript("interfaces/TreasureCollector.js"); Engine.LoadComponentScript("interfaces/Turretable.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("interfaces/Upkeep.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("GuiInterface.js"); Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], "GetNames": () => ({ "food": "Food", "metal": "Metal", "stone": "Stone", "wood": "Wood" }), "GetResource": resource => ({ "aiAnalysisInfluenceGroup": resource == "food" ? "ignore" : resource == "wood" ? "abundant" : "sparse" }) }; var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface"); AddMock(SYSTEM_ENTITY, IID_Barter, { "GetPrices": function() { return { "buy": { "food": 150 }, "sell": { "food": 25 } }; } }); AddMock(SYSTEM_ENTITY, IID_EndGameManager, { "GetVictoryConditions": () => ["conquest", "wonder"], "GetAlliedVictory": function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": function() { return 2; }, "GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; }, "GetMaxWorldPopulation": function() {} }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "GetLosVisibility": function(ent, player) { return "visible"; }, "GetLosCircular": function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetCurrentTemplateName": function(ent) { return "example"; }, "GetTemplate": function(name) { return ""; } }); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetTime": function() { return 0; }, "SetTimeout": function(ent, iid, funcname, time, data) { return 0; } }); AddMock(100, IID_Player, { "entity": 100, "GetColor": function() { return { "r": 1, "g": 1, "b": 1, "a": 1 }; }, "CanControlAllUnits": function() { return false; }, "GetPopulationCount": function() { return 10; }, "GetPopulationLimit": function() { return 20; }, "GetMaxPopulation": function() { return 200; }, "GetResourceCounts": function() { return { "food": 100 }; }, "GetResourceGatherers": function() { return { "food": 1 }; }, "GetPanelEntities": function() { return []; }, "IsTrainingBlocked": function() { return false; }, "GetState": function() { return "active"; }, - "GetTeam": function() { return -1; }, - "GetLockTeams": function() { return false; }, "GetCheatsEnabled": function() { return false; }, - "GetDiplomacy": function() { return [-1, 1]; }, - "IsAlly": function() { return false; }, - "IsMutualAlly": function() { return false; }, - "IsNeutral": function() { return false; }, - "IsEnemy": function() { return true; }, "GetDisabledTemplates": function() { return {}; }, "GetDisabledTechnologies": function() { return {}; }, "CanBarter": function() { return false; }, - "GetSpyCostMultiplier": function() { return 1; }, - "HasSharedDropsites": function() { return false; }, - "HasSharedLos": function() { return false; } + "GetSpyCostMultiplier": function() { return 1; } +}); + +AddMock(100, IID_Diplomacy, { + "GetTeam": () => -1, + "IsTeamLocked": () => false, + "GetDiplomacy": () => [-1, 1], + "IsAlly": () => false, + "IsMutualAlly": () => false, + "IsNeutral": () => false, + "IsEnemy": () => true, + "HasSharedDropsites": () => false, + "HasSharedLos": () => false, }); AddMock(100, IID_Identity, { "GetName": function() { return "Player 1"; }, "GetCiv": function() { return "gaia"; }, "GetRankTechName": function() { return undefined; } }); AddMock(100, IID_EntityLimits, { "GetLimits": function() { return { "Foo": 10 }; }, "GetCounts": function() { return { "Foo": 5 }; }, "GetLimitChangers": function() { return { "Foo": {} }; }, "GetMatchCounts": function() { return { "Bar": 0 }; } }); AddMock(100, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(100, IID_StatisticsTracker, { "GetBasicStatistics": function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, "GetSequences": function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, "IncreaseTrainedUnitsCounter": function() { return 1; }, "IncreaseConstructedBuildingsCounter": function() { return 1; }, "IncreaseBuiltCivCentresCounter": function() { return 1; } }); AddMock(101, IID_Player, { "entity": 101, "GetColor": function() { return { "r": 1, "g": 0, "b": 0, "a": 1 }; }, "CanControlAllUnits": function() { return true; }, "GetPopulationCount": function() { return 40; }, "GetPopulationLimit": function() { return 30; }, "GetMaxPopulation": function() { return 300; }, "GetResourceCounts": function() { return { "food": 200 }; }, "GetResourceGatherers": function() { return { "food": 3 }; }, "GetPanelEntities": function() { return []; }, "IsTrainingBlocked": function() { return false; }, "GetState": function() { return "active"; }, - "GetTeam": function() { return -1; }, - "GetLockTeams": function() {return false; }, "GetCheatsEnabled": function() { return false; }, - "GetDiplomacy": function() { return [-1, 1]; }, - "IsAlly": function() { return true; }, - "IsMutualAlly": function() {return false; }, - "IsNeutral": function() { return false; }, - "IsEnemy": function() { return false; }, "GetDisabledTemplates": function() { return {}; }, "GetDisabledTechnologies": function() { return {}; }, "CanBarter": function() { return false; }, "GetSpyCostMultiplier": function() { return 1; }, - "HasSharedDropsites": function() { return false; }, - "HasSharedLos": function() { return false; } +}); + +AddMock(101, IID_Diplomacy, { + "GetTeam": () => -1, + "IsTeamLocked": () => false, + "GetDiplomacy": () => [-1, 1], + "IsAlly": () => true, + "IsMutualAlly": () => false, + "IsNeutral": () => false, + "IsEnemy": () => false, + "HasSharedDropsites": () => false, + "HasSharedLos": () => false, }); AddMock(101, IID_Identity, { "GetName": function() { return "Player 2"; }, "GetCiv": function() { return "mace"; }, "GetRankTechName": function() { return undefined; } }); AddMock(101, IID_EntityLimits, { "GetLimits": function() { return { "Bar": 20 }; }, "GetCounts": function() { return { "Bar": 0 }; }, "GetLimitChangers": function() { return { "Bar": {} }; }, "GetMatchCounts": function() { return { "Foo": 0 }; } }); AddMock(101, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(101, IID_StatisticsTracker, { "GetBasicStatistics": function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, "GetSequences": function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, "IncreaseTrainedUnitsCounter": function() { return 1; }, "IncreaseConstructedBuildingsCounter": function() { return 1; }, "IncreaseBuiltCivCentresCounter": function() { return 1; } }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "entity": 100, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "resourceGatherers": { "food": 1 }, "trainingBlocked": false, "state": "active", "team": -1, - "teamsLocked": false, + "teamLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 } }, { "name": "Player 2", "civ": "mace", "color": { "r": 1, "g": 0, "b": 0, "a": 1 }, "entity": 101, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "resourceGatherers": { "food": 3 }, "trainingBlocked": false, "state": "active", "team": -1, - "teamsLocked": false, + "teamLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 } } ], "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], "alliedVictory": false, "maxWorldPopulation": undefined }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "entity": 100, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "resourceGatherers": { "food": 1 }, "trainingBlocked": false, "state": "active", "team": -1, - "teamsLocked": false, + "teamLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } }, { "name": "Player 2", "civ": "mace", "color": { "r": 1, "g": 0, "b": 0, "a": 1 }, "entity": 101, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "resourceGatherers": { "food": 3 }, "trainingBlocked": false, "state": "active", "team": -1, - "teamsLocked": false, + "teamLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } } ], "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], "alliedVictory": false, "maxWorldPopulation": undefined }); AddMock(10, IID_Builder, { "GetEntitiesList": function() { return ["test1", "test2"]; }, }); AddMock(10, IID_Health, { "GetHitpoints": function() { return 50; }, "GetMaxHitpoints": function() { return 60; }, "IsRepairable": function() { return false; }, "IsUnhealable": function() { return false; } }); AddMock(10, IID_Identity, { "GetClassesList": function() { return ["class1", "class2"]; }, "GetRank": function() { return "foo"; }, "GetSelectionGroupName": function() { return "Selection Group Name"; }, "HasClass": function() { return true; }, "IsUndeletable": function() { return false; }, "IsControllable": function() { return true; }, "GetRankTechName": function() { return undefined; } }); AddMock(10, IID_Position, { "GetTurretParent": function() { return INVALID_ENTITY; }, "GetPosition": function() { return { "x": 1, "y": 2, "z": 3 }; }, "IsInWorld": function() { return true; } }); AddMock(10, IID_ResourceTrickle, { "GetInterval": () => 1250, "GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 }) }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), { "id": 10, "player": INVALID_PLAYER, "template": "example", "identity": { "rank": "foo", "rankTechName": undefined, "classes": ["class1", "class2"], "selectionGroupName": "Selection Group Name", "canDelete": true, "controllable": true, }, "position": { "x": 1, "y": 2, "z": 3 }, "hitpoints": 50, "maxHitpoints": 60, "needsRepair": false, "needsHeal": true, "builder": true, "visibility": "visible", "isBarterMarket": true, "resourceTrickle": { "interval": 1250, "rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 } } }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js (revision 27722) @@ -1,223 +1,222 @@ Engine.LoadHelperScript("Player.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/TurretHolder.js"); Engine.LoadComponentScript("interfaces/Turretable.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("TurretHolder.js"); Engine.LoadComponentScript("Turretable.js"); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id }); const player = 1; const enemyPlayer = 2; const alliedPlayer = 3; const turretHolderID = 9; const entitiesToTest = [10, 11, 12, 13]; AddMock(turretHolderID, IID_Ownership, { "GetOwner": () => player }); AddMock(turretHolderID, IID_Position, { "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "IsInWorld": () => true }); for (let entity of entitiesToTest) { AddMock(entity, IID_Position, { "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "SetTurretParent": (parent, offset) => {}, "IsInWorld": () => true }); AddMock(entity, IID_Ownership, { "GetOwner": () => player }); } -AddMock(player, IID_Player, { +AddMock(player, IID_Diplomacy, { "IsAlly": id => id != enemyPlayer, "IsMutualAlly": id => id != enemyPlayer, - "GetPlayerID": () => player }); -AddMock(alliedPlayer, IID_Player, { +AddMock(alliedPlayer, IID_Diplomacy, { "IsAlly": id => true, "IsMutualAlly": id => true, - "GetPlayerID": () => alliedPlayer }); let cmpTurretHolder = ConstructComponent(turretHolderID, "TurretHolder", { "TurretPoints": { "archer1": { "X": "12.0", "Y": "5.", "Z": "6.0" }, "archer2": { "X": "15.0", "Y": "5.0", "Z": "6.0", "AllowedClasses": { "_string": "Siege Trader" } }, "archer3": { "X": "15.0", "Y": "5.0", "Z": "6.0", "AllowedClasses": { "_string": "Siege Infantry+Ranged Infantry+Cavalry" } } } }); let siegeEngineID = entitiesToTest[0]; AddMock(siegeEngineID, IID_Identity, { "GetClassesList": () => ["Siege"] }); let archerID = entitiesToTest[1]; AddMock(archerID, IID_Identity, { "GetClassesList": () => ["Infantry", "Ranged"] }); let cavID = entitiesToTest[2]; AddMock(cavID, IID_Identity, { "GetClassesList": () => ["Infantry", "Cavalry"] }); let infID = entitiesToTest[3]; AddMock(infID, IID_Identity, { "GetClassesList": () => ["Infantry"] }); // Test visible garrisoning restrictions. TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[0]), true); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[1]), true); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[2]), true); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[0]), true); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[1]), false); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[2]), true); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[0]), true); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[1]), false); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[2]), true); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[0]), true); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[1]), false); TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[2]), false); // Test that one cannot leave a turret that is not occupied. TS_ASSERT(!cmpTurretHolder.LeaveTurretPoint(archerID)); // Test occupying a turret. TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(archerID)); TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(archerID)); TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(archerID)); // We're not occupying a turret that we can't occupy. TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(archerID, cmpTurretHolder.turretPoints[1])); TS_ASSERT(!cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[1])); TS_ASSERT(!cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[0])); TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[2])); // Leave turrets. TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID)); TS_ASSERT(!cmpTurretHolder.LeaveTurretPoint(cavID, false, cmpTurretHolder.turretPoints[1])); TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(cavID, false, cmpTurretHolder.turretPoints[2])); // Incremental Turret creation. cmpTurretHolder = ConstructComponent(turretHolderID, "TurretHolder", { "TurretPoints": { "Turret": { "X": "15.0", "Y": "5.0", "Z": "6.0", "Template": "units/iber/cavalry_javelineer_c" } } }); let spawned = 100; Engine.AddEntity = function() { ++spawned; if(spawned > 101) { ConstructComponent(spawned, "Turretable", {}); } if(spawned > 102) { AddMock(spawned, IID_Ownership, { "GetOwner": () => player, "SetOwner": () => {} }); } if(spawned > 103) { AddMock(spawned, IID_Position, { "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "SetTurretParent": () => {}, "IsInWorld": () => true }); } return spawned; } const GetUpgradedTemplate = (_, template) => template === "units/iber/cavalry_javelineer_b" ? "units/iber/cavalry_javelineer_a" : template; Engine.RegisterGlobal("GetUpgradedTemplate", GetUpgradedTemplate); cmpTurretHolder.OnOwnershipChanged({ "to": 1, "from": INVALID_PLAYER }); TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(spawned)); cmpTurretHolder.OnOwnershipChanged({ "to": 1, "from": INVALID_PLAYER }); TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(spawned)); cmpTurretHolder.OnOwnershipChanged({ "to": 1, "from": INVALID_PLAYER }); TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(spawned)); cmpTurretHolder.OnOwnershipChanged({ "to": 1, "from": INVALID_PLAYER }); TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(spawned)); // Normal turret creation. Engine.AddEntity = function(t) { ++spawned; // Check that we're using the upgraded template. TS_ASSERT(t, "units/iber/cavalry_javelineer_a"); ConstructComponent(spawned, "Turretable", {}); AddMock(spawned, IID_Ownership, { "GetOwner": () => player, "SetOwner": () => {} }); AddMock(spawned, IID_Position, { "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "SetTurretParent": () => {}, "IsInWorld": () => true }); return spawned; } cmpTurretHolder = ConstructComponent(turretHolderID, "TurretHolder", { "TurretPoints": { "Turret": { "X": "15.0", "Y": "5.0", "Z": "6.0", "Template": "units/iber/cavalry_javelineer_b" } } }); cmpTurretHolder.OnOwnershipChanged({ "to": 1, "from": INVALID_PLAYER }); TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(spawned)); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Attack.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Attack.js (revision 27722) @@ -1,381 +1,381 @@ /** * Provides attack and damage-related helpers. */ function AttackHelper() {} const DirectEffectsSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; const StatusEffectsSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + DirectEffectsSchema + "" + "" + "" + "" + "" + ModificationsSchema + "" + "" + "" + "Ignore" + "Extend" + "Replace" + "Stack" + "" + "" + "" + "" + "" + ""; /** * Builds a RelaxRNG schema of possible attack effects. * See globalscripts/AttackEffects.js for possible elements. * Attacks may also have a "Bonuses" element. * * @return {string} - RelaxNG schema string. */ AttackHelper.prototype.BuildAttackEffectsSchema = function() { return "" + "" + "" + DirectEffectsSchema + StatusEffectsSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; }; /** * Returns a template-like object of attack effects. */ AttackHelper.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity) { let ret = {}; if (template.Damage) { ret.Damage = {}; let applyMods = damageType => ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity); for (let damageType in template.Damage) ret.Damage[damageType] = applyMods(damageType); } if (template.Capture) ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity); if (template.ApplyStatus) ret.ApplyStatus = this.GetStatusEffectsData(valueModifRoot, template.ApplyStatus, entity); if (template.Bonuses) ret.Bonuses = template.Bonuses; return ret; }; AttackHelper.prototype.GetStatusEffectsData = function(valueModifRoot, template, entity) { let result = {}; for (let effect in template) { let statusTemplate = template[effect]; result[effect] = { "Duration": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Duration", +(statusTemplate.Duration || 0), entity), "Interval": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Interval", +(statusTemplate.Interval || 0), entity), "Stackability": statusTemplate.Stackability }; Object.assign(result[effect], this.GetAttackEffectsData(valueModifRoot + "/ApplyStatus" + effect, statusTemplate, entity)); if (statusTemplate.Modifiers) result[effect].Modifiers = this.GetStatusEffectsModifications(valueModifRoot, statusTemplate.Modifiers, entity, effect); } return result; }; AttackHelper.prototype.GetStatusEffectsModifications = function(valueModifRoot, template, entity, effect) { let modifiers = {}; for (let modifier in template) { let modifierTemplate = template[modifier]; modifiers[modifier] = { "Paths": modifierTemplate.Paths, "Affects": modifierTemplate.Affects }; if (modifierTemplate.Add !== undefined) modifiers[modifier].Add = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Add", +modifierTemplate.Add, entity); if (modifierTemplate.Multiply !== undefined) modifiers[modifier].Multiply = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Multiply", +modifierTemplate.Multiply, entity); if (modifierTemplate.Replace !== undefined) modifiers[modifier].Replace = modifierTemplate.Replace; } return modifiers; }; /** * Calculate the total effect taking bonus and resistance into account. * * @param {number} target - The target of the attack. * @param {Object} effectData - The effects calculate the effect for. * @param {string} effectType - The type of effect to apply (e.g. Damage, Capture or ApplyStatus). * @param {number} bonusMultiplier - The factor to multiply the total effect with. * @param {Object} cmpResistance - Optionally the resistance component of the target. * * @return {number} - The total value of the effect. */ AttackHelper.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance) { let total = 0; if (!cmpResistance) cmpResistance = Engine.QueryInterface(target, IID_Resistance); let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {}; if (effectType == "Damage") for (let type in effectData.Damage) total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage ? resistanceStrengths.Damage[type] || 0 : 0); else if (effectType == "Capture") { total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture || 0); // If Health is lower we are more susceptible to capture attacks. let cmpHealth = Engine.QueryInterface(target, IID_Health); if (cmpHealth) total /= 0.1 + 0.9 * cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints(); } if (effectType != "ApplyStatus") return total * bonusMultiplier; if (!resistanceStrengths.ApplyStatus) return effectData[effectType]; let result = {}; for (let statusEffect in effectData[effectType]) { if (!resistanceStrengths.ApplyStatus[statusEffect]) { result[statusEffect] = effectData[effectType][statusEffect]; continue; } if (randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance)) continue; result[statusEffect] = effectData[effectType][statusEffect]; if (effectData[effectType][statusEffect].Duration) result[statusEffect].Duration = effectData[effectType][statusEffect].Duration * resistanceStrengths.ApplyStatus[statusEffect].duration; } return result; }; /** * Get the list of players affected by the damage. * @param {number} attackerOwner - The player id of the attacker. * @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged. * @return {number[]} The ids of players need to be damaged. */ AttackHelper.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire) { if (!friendlyFire) - return QueryPlayerIDInterface(attackerOwner).GetEnemies(); + return QueryPlayerIDInterface(attackerOwner, IID_Diplomacy).GetEnemies(); return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); }; /** * Damages units around a given origin. * @param {Object} data - The data sent by the caller. * @param {string} data.type - The type of damage. * @param {Object} data.attackData - The attack data. * @param {number} data.attacker - The entity id of the attacker. * @param {number} data.attackerOwner - The player id of the attacker. * @param {Vector2D} data.origin - The origin of the projectile hit. * @param {number} data.radius - The radius of the splash damage. * @param {string} data.shape - The shape of the radius. * @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage. * @param {boolean} data.friendlyFire - A flag indicating if allied entities also ought to be damaged. */ AttackHelper.prototype.CauseDamageOverArea = function(data) { let nearEnts = PositionHelper.EntitiesNearPoint(data.origin, data.radius, this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire)); let damageMultiplier = 1; let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin. for (let ent of nearEnts) { // Correct somewhat for the entity's obstruction radius. // TODO: linear falloff should arguably use something cleverer. let distance = cmpObstructionManager.DistanceToPoint(ent, data.origin.x, data.origin.y); if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction damageMultiplier = 1 - distance * distance / (data.radius * data.radius); else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles) { // The entity has a position here since it was returned by the range manager. let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D(); let relativePos = entityPosition.sub(data.origin).normalize().mult(distance); // Get the position relative to the missile direction. let direction = Vector2D.from3D(data.direction); let parallelPos = relativePos.dot(direction); let perpPos = relativePos.cross(direction); // The width of linear splash is one fifth of the normal splash radius. let width = data.radius / 5; // Check that the unit is within the distance splash width of the line starting at the missile's // landing point which extends in the direction of the missile for length splash radius. if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) * (1 - perpPos * perpPos / (width * width)); else damageMultiplier = 0; } else // In case someone calls this function with an invalid shape. { warn("The " + data.shape + " splash damage shape is not implemented!"); } // The RangeManager can return units that are too far away (due to approximations there) // so the multiplier can end up below 0. damageMultiplier = Math.max(0, damageMultiplier); data.type += ".Splash"; this.HandleAttackEffects(ent, data, damageMultiplier); } }; /** * Handle an attack peformed on an entity. * * @param {number} target - The targetted entityID. * @param {Object} data - The data of the attack. * @param {string} data.type - The type of attack that was performed (e.g. "Melee" or "Capture"). * @param {Object} data.effectData - The effects use. * @param {number} data.attacker - The entityID that attacked us. * @param {number} data.attackerOwner - The playerID that owned the attacker when the attack was performed. * @param {number} bonusMultiplier - The factor to multiply the total effect with, defaults to 1. * * @return {boolean} - Whether we handled the attack. */ AttackHelper.prototype.HandleAttackEffects = function(target, data, bonusMultiplier = 1) { let cmpResistance = Engine.QueryInterface(target, IID_Resistance); if (cmpResistance && cmpResistance.IsInvulnerable()) return false; bonusMultiplier *= !data.attackData.Bonuses ? 1 : this.GetAttackBonus(data.attacker, target, data.type, data.attackData.Bonuses); let targetState = {}; for (let receiver of g_AttackEffects.Receivers()) { if (!data.attackData[receiver.type]) continue; let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]); if (!cmpReceiver) continue; Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, data.attackData, receiver.type, bonusMultiplier, cmpResistance), data.attacker, data.attackerOwner)); } if (!Object.keys(targetState).length) return false; Engine.PostMessage(target, MT_Attacked, { "type": data.type, "target": target, "attacker": data.attacker, "attackerOwner": data.attackerOwner, "damage": -(targetState.healthChange || 0), "capture": targetState.captureChange || 0, "statusEffects": targetState.inflictedStatuses || [], "fromStatusEffect": !!data.attackData.StatusEffect, }); // We do not want an entity to get XP from active Status Effects. if (!!data.attackData.StatusEffect) return true; let cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion); if (cmpPromotion && targetState.xp) cmpPromotion.IncreaseXp(targetState.xp); return true; }; /** * Calculates the attack damage multiplier against a target. * @param {number} source - The source entity's id. * @param {number} target - The target entity's id. * @param {string} type - The type of attack. * @param {Object} template - The bonus' template. * @return {number} - The source entity's attack bonus against the specified target. */ AttackHelper.prototype.GetAttackBonus = function(source, target, type, template) { let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return 1; let attackBonus = 1; let targetClasses = cmpIdentity.GetClassesList(); let targetCiv = cmpIdentity.GetCiv(); // Multiply the bonuses for all matching classes. for (let key in template) { let bonus = template[key]; if (bonus.Civ && bonus.Civ !== targetCiv) continue; if (!bonus.Classes || MatchesClassList(targetClasses, bonus.Classes)) attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source); } return attackBonus; }; Engine.RegisterGlobal("AttackHelper", new AttackHelper()); Engine.RegisterGlobal("g_AttackEffects", new AttackEffects()); Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaia.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaia.xml (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/players/gaia.xml (revision 27722) @@ -1,45 +1,47 @@ + + + + gaia Gaia emblems/emblem_gaia.png true - - 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 special/formations/null special/formations/box special/formations/column_closed special/formations/line_closed special/formations/column_open special/formations/line_open special/formations/flank special/formations/battle_line special/formations/skirmish special/formations/wedge special/formations/phalanx special/formations/syntagma special/formations/testudo special/formations/anti_cavalry special/formations/scatter Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 27722) @@ -1,407 +1,411 @@ AttackEffects = class AttackEffects { constructor() {} Receivers() { return [{ "type": "Damage", "IID": "IID_Health", "method": "TakeDamage" }, { "type": "Capture", "IID": "IID_Capturable", "method": "Capture" }, { "type": "ApplyStatus", "IID": "IID_StatusEffectsReceiver", "method": "ApplyStatus" }]; } }; Engine.LoadHelperScript("Attack.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("Attack.js"); let entityID = 903; function attackComponentTest(defenderClass, isEnemy, test_function) { let playerEnt1 = 5; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => playerEnt1 }); AddMock(playerEnt1, IID_Player, { "GetPlayerID": () => 1, + }); + + AddMock(playerEnt1, IID_Diplomacy, { "IsEnemy": () => isEnemy }); let attacker = entityID; AddMock(attacker, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 5, "GetPosition2D": () => new Vector2D(1, 2) }); AddMock(attacker, IID_Ownership, { "GetOwner": () => 1 }); let cmpAttack = ConstructComponent(attacker, "Attack", { "Melee": { "Damage": { "Hack": 11, "Pierce": 5, "Crush": 0 }, "MinRange": 3, "MaxRange": 5, "PreferredClasses": { "_string": "FemaleCitizen" }, "RestrictedClasses": { "_string": "Elephant Archer" }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 2 } } }, "Ranged": { "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 }, "MinRange": 10, "MaxRange": 80, "PrepareTime": 300, "RepeatTime": 500, "Projectile": { "Speed": 10, "Spread": 2, "Gravity": 1, "FriendlyFire": "false" }, "PreferredClasses": { "_string": "Archer" }, "RestrictedClasses": { "_string": "Elephant" }, "Splash": { "Shape": "Circular", "Range": 10, "FriendlyFire": "false", "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } } }, "Capture": { "Capture": 8, "MaxRange": 10, }, "Slaughter": {}, "StatusEffect": { "ApplyStatus": { "StatusInternalName": { "StatusName": "StatusShownName", "ApplierTooltip": "ApplierTooltip", "ReceiverTooltip": "ReceiverTooltip", "Duration": 5000, "Stackability": "Stacks", "Modifiers": { "SE": { "Paths": { "_string": "Health/Max" }, "Affects": { "_string": "Unit" }, "Add": 10 } } } }, "MinRange": "10", "MaxRange": "80" } }); let defender = ++entityID; AddMock(defender, IID_Identity, { "GetClassesList": () => [defenderClass], "HasClass": className => className == defenderClass, "GetCiv": () => "civ" }); AddMock(defender, IID_Ownership, { "GetOwner": () => 1 }); AddMock(defender, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 0 }); AddMock(defender, IID_Health, { "GetHitpoints": () => 100 }); AddMock(defender, IID_Resistance, { }); test_function(attacker, cmpAttack, defender); } // Validate template getter functions attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => { TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged", "Capture"]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged"]), ["Melee", "Ranged"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "!Melee"]), []); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee"]), ["Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Melee", "!Ranged"]), ["Melee", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), { "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), { "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("StatusEffect"), { "ApplyStatus": { "StatusInternalName": { "Duration": 5000, "Interval": 0, "Stackability": "Stacks", "Modifiers": { "SE": { "Paths": { "_string": "Health/Max" }, "Affects": { "_string": "Unit" }, "Add": 10 } } } } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), { "prepare": 300, "repeat": 500 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Ranged"), 500); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), { "prepare": 0, "repeat": 1000 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Capture"), 1000); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashData("Ranged"), { "attackData": { "Damage": { "Hack": 0, "Pierce": 15, "Crush": 35, }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } }, "friendlyFire": false, "radius": 10, "shape": "Circular" }); }); for (let className of ["Infantry", "Cavalry"]) attackComponentTest(className, true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2); TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null); let getAttackBonus = (s, t, e, splash) => AttackHelper.GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1); }); // CanAttack rejects elephant attack due to RestrictedClasses attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false); }); function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false) { attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack); }); attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAllyAttack); }); } testGetBestAttackAgainst("FemaleCitizen", "Melee", undefined); testGetBestAttackAgainst("Archer", "Ranged", undefined); testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter"); testGetBestAttackAgainst("Structure", "Capture", "Capture", true); testGetBestAttackAgainst("Structure", "Ranged", undefined, false); function testAttackPreference() { const attacker = 5; let cmpAttack = ConstructComponent(attacker, "Attack", { "Melee": { "Damage": { "Crush": 0 }, "MinRange": 3, "MaxRange": 5, "PreferredClasses": { "_string": "FemaleCitizen Unit+!Ship" }, "RestrictedClasses": { "_string": "Elephant Archer" }, } }); AddMock(attacker+1, IID_Identity, { "GetClassesList": () => ["FemaleCitizen", "Unit"] }); AddMock(attacker+2, IID_Identity, { "GetClassesList": () => ["Unit"] }); AddMock(attacker+3, IID_Identity, { "GetClassesList": () => ["Unit", "Ship"] }); AddMock(attacker+4, IID_Identity, { "GetClassesList": () => ["SomethingElse"] }); TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+1), 0); TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+2), 1); TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+3), undefined); TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), undefined); } testAttackPreference(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js (revision 27722) @@ -1,194 +1,195 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Cost.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Builder.js"); Engine.LoadComponentScript("Health.js"); Engine.LoadComponentScript("Repairable.js"); Engine.LoadComponentScript("Timer.js"); const builderId = 6; const target = 7; const playerId = 1; const playerEntityID = 2; AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": () => true }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true }); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal); function testEntitiesList() { let cmpBuilder = ConstructComponent(builderId, "Builder", { "Rate": "1.0", "Entities": { "_string": "structures/{civ}/barracks structures/{civ}/civil_centre structures/{native}/house" } }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), []); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => playerEntityID }); AddMock(playerEntityID, IID_Player, { "GetDisabledTemplates": () => ({}), "GetPlayerID": () => playerId }); AddMock(playerEntityID, IID_Identity, { "GetCiv": () => "iber", }); AddMock(builderId, IID_Ownership, { "GetOwner": () => playerId }); AddMock(builderId, IID_Identity, { "GetCiv": () => "iber" }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/barracks", "structures/iber/civil_centre", "structures/iber/house"]); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": name => name == "structures/iber/civil_centre" }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/civil_centre"]); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true }); AddMock(playerEntityID, IID_Player, { "GetDisabledTemplates": () => ({ "structures/athen/barracks": true }), "GetPlayerID": () => playerId }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/barracks", "structures/iber/civil_centre", "structures/iber/house"]); AddMock(playerEntityID, IID_Player, { "GetDisabledTemplates": () => ({ "structures/iber/barracks": true }), "GetPlayerID": () => playerId }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/civil_centre", "structures/iber/house"]); AddMock(playerEntityID, IID_Player, { "GetDisabledTemplates": () => ({ "structures/athen/barracks": true }), "GetPlayerID": () => playerId }); AddMock(playerEntityID, IID_Identity, { "GetCiv": () => "athen", }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/athen/civil_centre", "structures/iber/house"]); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 2, "min": 0 }); AddMock(builderId, IID_Obstruction, { "GetSize": () => 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 3, "min": 0 }); } testEntitiesList(); function testBuildingFoundation() { let cmpBuilder = ConstructComponent(builderId, "Builder", { "Rate": "1.0", "Entities": { "_string": "" } }); - AddMock(playerEntityID, IID_Player, { + AddMock(playerEntityID, IID_Diplomacy, { "IsAlly": (p) => p == playerId }); AddMock(target, IID_Ownership, { "GetOwner": () => playerId }); let increased = false; AddMock(target, IID_Foundation, { "Build": (entity, amount) => { increased = true; TS_ASSERT_EQUALS(amount, 1); }, "AddBuilder": () => {} }); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); TS_ASSERT(cmpBuilder.StartRepairing(target)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(increased); increased = false; cmpTimer.OnUpdate({ "turnLength": 2 }); TS_ASSERT(increased); } testBuildingFoundation(); function testRepairing() { - AddMock(playerEntityID, IID_Player, { + AddMock(playerEntityID, IID_Diplomacy, { "IsAlly": (p) => p == playerId }); let cmpBuilder = ConstructComponent(builderId, "Builder", { "Rate": "1.0", "Entities": { "_string": "" } }); AddMock(target, IID_Ownership, { "GetOwner": () => playerId }); AddMock(target, IID_Cost, { "GetBuildTime": () => 100 }); let cmpTargetHealth = ConstructComponent(target, "Health", { "Max": 100, "RegenRate": 0, "IdleRegenRate": 0, "DeathType": "vanish", "Unhealable": false }); cmpTargetHealth.SetHitpoints(50); DeleteMock(target, IID_Foundation); let cmpTargetRepairable = ConstructComponent(target, "Repairable", { "RepairTimeRatio": 1, }); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); TS_ASSERT(cmpTargetRepairable.IsRepairable()); TS_ASSERT(cmpBuilder.StartRepairing(target)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 51); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); cmpTargetRepairable.SetRepairability(false); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); cmpTargetRepairable.SetRepairability(true); // Check that we indeed stopped - shouldn't restart on its own. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); TS_ASSERT(cmpBuilder.StartRepairing(target)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 53); } testRepairing(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 27722) @@ -1,707 +1,708 @@ AttackEffects = class AttackEffects { constructor() {} Receivers() { return [{ "type": "Damage", "IID": "IID_Health", "method": "TakeDamage" }]; } }; Engine.LoadHelperScript("Attack.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Position.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/DelayedDamage.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Attack.js"); Engine.LoadComponentScript("DelayedDamage.js"); Engine.LoadComponentScript("Timer.js"); function Test_Generic() { ResetState(); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); cmpTimer.OnUpdate({ "turnLength": 1 }); let attacker = 11; let atkPlayerEntity = 1; let attackerOwner = 6; let cmpAttack = ConstructComponent(attacker, "Attack", { "Ranged": { "Damage": { "Crush": 5, }, "MaxRange": 50, "MinRange": 0, "EffectDelay": 0, "Projectile": { "Speed": 75.0, "Spread": 0.5, "Gravity": 9.81, "FriendlyFire": "false", "LaunchPoint": { "@y": 3 } } } }); let damage = 5; let target = 21; let targetOwner = 7; let targetPos = new Vector3D(3, 0, 3); let type = "Melee"; let damageTaken = false; cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage }); let data = { "type": "Melee", "attackData": { "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage }, }, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "position": targetPos, "projectileId": 9, "direction": new Vector3D(1, 0, 0) }; - AddMock(atkPlayerEntity, IID_Player, { + AddMock(atkPlayerEntity, IID_Diplomacy, { "GetEnemies": () => [targetOwner] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => atkPlayerEntity, "GetAllPlayers": () => [0, 1, 2, 3, 4] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(target, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "GetHeightAt": () => 0, "IsInWorld": () => true, }); AddMock(target, IID_Health, { "TakeDamage": (amount, __, ___) => { damageTaken = true; return { "healthChange": -amount }; }, }); AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { "Hit": () => { damageTaken = true; }, }); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "type": type, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": damage, "capture": 0, "statusEffects": [], "fromStatusEffect": false }, message); }; AddMock(target, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(attacker, IID_Ownership, { "GetOwner": () => attackerOwner, }); AddMock(attacker, IID_Position, { "GetPosition": () => new Vector3D(2, 0, 3), "GetRotation": () => new Vector3D(1, 2, 3), "IsInWorld": () => true, }); function TestDamage() { cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(damageTaken); damageTaken = false; } AttackHelper.HandleAttackEffects(target, data); TestDamage(); data.type = "Ranged"; type = data.type; AttackHelper.HandleAttackEffects(target, data); TestDamage(); // Check for damage still being dealt if the attacker dies cmpAttack.PerformAttack("Ranged", target); Engine.DestroyEntity(attacker); TestDamage(); atkPlayerEntity = 1; - AddMock(atkPlayerEntity, IID_Player, { + AddMock(atkPlayerEntity, IID_Diplomacy, { "GetEnemies": () => [2, 3] }); TS_ASSERT_UNEVAL_EQUALS(AttackHelper.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); TS_ASSERT_UNEVAL_EQUALS(AttackHelper.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); } Test_Generic(); function TestLinearSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const attacker = 50; const attackerOwner = 1; const origin = new Vector2D(0, 0); let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": attacker, "attackerOwner": attackerOwner, "origin": origin, "radius": 10, "shape": "Linear", "direction": new Vector3D(1, 747, 0), "friendlyFire": false, }; let fallOff = function(x, y) { return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius)); }; let hitEnts = new Set(); - AddMock(attackerOwner, IID_Player, { + AddMock(attackerOwner, IID_Diplomacy, { "GetEnemies": () => [2] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => attackerOwner, "GetAllPlayers": () => [0, 1, 2] }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62], }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "DistanceToPoint": (ent) => ({ "60": Math.sqrt(9.25), "61": 0, "62": Math.sqrt(29) }[ent]) }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, -0.5), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(5, 2), }); AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(60); TS_ASSERT_EQUALS(amount, 100 * fallOff(3, -0.5)); return { "healthChange": -amount }; } }); AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(61); TS_ASSERT_EQUALS(amount, 100 * fallOff(0, 0)); return { "healthChange": -amount }; } }); AddMock(62, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(62); // Minor numerical precision issues make this necessary TS_ASSERT(amount < 0.00001); return { "healthChange": -amount }; } }); AttackHelper.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); data.direction = new Vector3D(0.6, 747, 0.8); AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(60); TS_ASSERT_EQUALS(amount, 100 * fallOff(1, 2)); return { "healthChange": -amount }; } }); AttackHelper.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); } TestLinearSplashDamage(); function TestCircularSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const radius = 10; let attackerOwner = 1; let fallOff = function(r) { return 1 - r * r / (radius * radius); }; - AddMock(attackerOwner, IID_Player, { + AddMock(attackerOwner, IID_Diplomacy, { "GetEnemies": () => [2] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => attackerOwner, "GetAllPlayers": () => [0, 1, 2] }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62, 64, 65], }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "DistanceToPoint": (ent, x, z) => ({ "60": 0, "61": 5, "62": 1, "63": Math.sqrt(85), "64": 10, "65": 2 }[ent]) }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, 4), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(3.6, 3.2), }); AddMock(63, IID_Position, { "GetPosition2D": () => new Vector2D(10, -10), }); // Target on the frontier of the shape (see distance above). AddMock(64, IID_Position, { "GetPosition2D": () => new Vector2D(9, -4), }); // Big target far away (see distance above). AddMock(65, IID_Position, { "GetPosition2D": () => new Vector2D(23, 4), }); AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(0)); return { "healthChange": -amount }; } }); AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(5)); return { "healthChange": -amount }; } }); AddMock(62, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(1)); return { "healthChange": -amount }; } }); AddMock(63, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT(false); } }); let cmphealth64 = AddMock(64, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 0); return { "healthChange": -amount }; } }); let spy64 = new Spy(cmphealth64, "TakeDamage"); let cmpHealth65 = AddMock(65, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(2)); return { "healthChange": -amount }; } }); let spy65 = new Spy(cmpHealth65, "TakeDamage"); AttackHelper.CauseDamageOverArea({ "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": 50, "attackerOwner": attackerOwner, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", "friendlyFire": false, }); TS_ASSERT_EQUALS(spy64._called, 1); TS_ASSERT_EQUALS(spy65._called, 1); } TestCircularSplashDamage(); function Test_MissileHit() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage"); let target = 60; let targetOwner = 1; let targetPos = new Vector3D(3, 10, 0); let hitEnts = new Set(); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetLatestTurnLength": () => 500 }); const radius = 10; let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } }, "target": 60, "attacker": 70, "attackerOwner": 1, "position": targetPos, "direction": new Vector3D(1, 0, 0), "projectileId": 9, "friendlyFire": "false", }; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id == 1 ? 10 : 11, "GetAllPlayers": () => [0, 1] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(60, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(60); TS_ASSERT_EQUALS(amount, 100); return { "healthChange": -amount }; } }); AddMock(60, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(70, IID_Ownership, { "GetOwner": () => 1, }); AddMock(70, IID_Position, { "GetPosition": () => new Vector3D(0, 0, 0), "GetRotation": () => new Vector3D(0, 0, 0), "IsInWorld": () => true, }); - AddMock(10, IID_Player, { + AddMock(10, IID_Diplomacy, { "GetEnemies": () => [2] }); cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(60)); hitEnts.clear(); // Target is a mirage: hit the parent. AddMock(60, IID_Mirage, { "GetParent": () => 61 }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "DistanceToPoint": (ent) => 0 }); AddMock(61, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.from3D(targetPos), "IsInWorld": () => true }); AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(61); TS_ASSERT_EQUALS(amount, 100); return { "healthChange": -amount }; } }); AddMock(61, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }) }); cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); // Make sure we don't corrupt other tests. DeleteMock(60, IID_Mirage); cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(60)); hitEnts.clear(); // The main target is not hit but another one is hit. AddMock(60, IID_Position, { "GetPosition": () => new Vector3D(900, 10, 0), "GetPreviousPosition": () => new Vector3D(900, 10, 0), "GetPosition2D": () => new Vector2D(900, 0), "IsInWorld": () => true }); AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(false); return { "healthChange": -amount }; } }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); // Add a splash damage. data.splash = {}; data.splash.friendlyFire = false; data.splash.radius = 10; data.splash.shape = "Circular"; data.splash.attackData = { "Damage": { "Hack": 0, "Pierce": 0, "Crush": 200 } }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61, 62] }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "DistanceToPoint": (ent) => ({ "61": 0, "62": 5 }[ent]) }); let dealtDamage = 0; AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(61); dealtDamage += amount; return { "healthChange": -amount }; } }); AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), "GetPosition2D": () => new Vector2D(8, 0), "IsInWorld": () => true, }); AddMock(62, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(62); TS_ASSERT_EQUALS(amount, 200 * 0.75); return { "healthChange": -amount }; } }); AddMock(62, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); // Add some hard counters bonus. Engine.DestroyEntity(62); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "DistanceToPoint": (ent) => 0 }); let bonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } }; let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } }; AddMock(61, IID_Identity, { "GetClassesList": () => ["Cavalry"], "GetCiv": () => "civ" }); data.attackData.Bonuses = bonus; cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200); dealtDamage = 0; hitEnts.clear(); data.splash.attackData.Bonuses = splashBonus; cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = undefined; cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = null; cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = {}; cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); // Test splash damage with friendly fire. data.splash = {}; data.splash.friendlyFire = true; data.splash.radius = 10; data.splash.shape = "Circular"; data.splash.attackData = { "Damage": { "Pierce": 0, "Crush": 200 } }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61, 62] }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "DistanceToPoint": (ent) => ({ "61": 0, "62": 5 }[ent]) }); dealtDamage = 0; AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(61); dealtDamage += amount; return { "healthChange": -amount }; } }); AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), "GetPosition2D": () => new Vector2D(8, 0), "IsInWorld": () => true, }); AddMock(62, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(62); TS_ASSERT_EQUALS(amount, 200 * 0.75); return { "healtChange": -amount }; } }); AddMock(62, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); } Test_MissileHit(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 27722) @@ -1,291 +1,290 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Garrisonable.js"); Engine.LoadComponentScript("GarrisonHolder.js"); const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33]; const garrisonHolderId = 15; const unitToGarrisonId = 24; const enemyUnitId = 34; const largeUnitId = 35; const player = 1; const friendlyPlayer = 2; const enemyPlayer = 3; let cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { "Max": "10", "List": { "_string": "Infantry+Cavalry" }, "EjectHealth": "0.1", "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": "1", "LoadingRange": "2.1", "Pickup": false }); AddMock(garrisonHolderId, IID_Ownership, { "GetOwner": () => player }); -AddMock(player, IID_Player, { +AddMock(player, IID_Diplomacy, { "IsAlly": id => id != enemyPlayer, "IsMutualAlly": id => id != enemyPlayer, - "GetPlayerID": () => player }); -AddMock(friendlyPlayer, IID_Player, { +AddMock(friendlyPlayer, IID_Diplomacy, { "IsAlly": id => true, "IsMutualAlly": id => true, - "GetPlayerID": () => friendlyPlayer }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": (ent, iid, funcname, time, data) => 1 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id }); for (let i = 24; i <= 35; ++i) { AddMock(i, IID_Identity, { "GetClassesList": () => ["Infantry", "Cavalry"], "GetSelectionGroupName": () => "mace_infantry_archer_a" }); if (i < 28) AddMock(i, IID_Ownership, { "GetOwner": () => player }); else if (i == 34) AddMock(i, IID_Ownership, { "GetOwner": () => enemyPlayer }); else AddMock(i, IID_Ownership, { "GetOwner": () => friendlyPlayer }); if (i == largeUnitId) AddMock(i, IID_Garrisonable, { "UnitSize": () => 9, "TotalSize": () => 9, "Garrison": (entity) => cmpGarrisonHolder.Garrison(i), "UnGarrison": () => cmpGarrisonHolder.Eject(i) }); else AddMock(i, IID_Garrisonable, { "UnitSize": () => 1, "TotalSize": () => 1, "Garrison": entity => cmpGarrisonHolder.Garrison(i), "UnGarrison": () => cmpGarrisonHolder.Eject(i) }); AddMock(i, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {} }); } AddMock(33, IID_Identity, { "GetClassesList": () => ["Infantry", "Cavalry"], "GetSelectionGroupName": () => "spart_infantry_archer_a" }); let testGarrisonAllowed = function() { TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(largeUnitId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(largeUnitId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true); for (let entity of garrisonedEntitiesList) TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(entity), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(largeUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadTemplate("spart_infantry_archer_a", 2, false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27, 28, 29, 30, 31, 32]); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAllByOwner(friendlyPlayer), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27]); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 4); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(null, false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 26, 27]); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 3); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(largeUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); }; // No health component yet.Pick testGarrisonAllowed(); AddMock(garrisonHolderId, IID_Health, { "GetHitpoints": () => 50, "GetMaxHitpoints": () => 600 }); cmpGarrisonHolder.AllowGarrisoning(true, "callerID1"); cmpGarrisonHolder.AllowGarrisoning(false, 5); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), false); cmpGarrisonHolder.AllowGarrisoning(true, 5); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.LoadingRange(), { "max": 2.1, "min": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetHealRate(), 1); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetAllowedClasses(), "Infantry+Cavalry"); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetCapacity(), 10); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(largeUnitId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), false); AddMock(garrisonHolderId, IID_Health, { "GetHitpoints": () => 600, "GetMaxHitpoints": () => 600 }); // No eject health. cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { "Max": 10, "List": { "_string": "Infantry+Cavalry" }, "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": 1, "LoadingRange": 2.1, "Pickup": false }); testGarrisonAllowed(); // Test entity renaming. let siegeEngineId = 44; AddMock(siegeEngineId, IID_Identity, { "GetClassesList": () => ["Siege"] }); let archerId = 45; AddMock(archerId, IID_Identity, { "GetClassesList": () => ["Infantry", "Ranged"] }); let originalClassList = "Infantry+Ranged Siege Cavalry"; cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { "Max": 10, "List": { "_string": originalClassList }, "EjectHealth": 0.1, "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": 1, "LoadingRange": 2.1, "Pickup": false }); let traderId = 32; AddMock(traderId, IID_Identity, { "GetClassesList": () => ["Trader"] }); AddMock(siegeEngineId, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {} }); let currentSiegePlayer = player; AddMock(siegeEngineId, IID_Ownership, { "GetOwner": () => currentSiegePlayer }); AddMock(siegeEngineId, IID_Garrisonable, { "UnitSize": () => 1, "TotalSize": () => 1, "Garrison": (entity, renamed) => cmpGarrisonHolder.Garrison(siegeEngineId, renamed), "UnGarrison": () => true }); let cavalryId = 46; AddMock(cavalryId, IID_Identity, { "GetClassesList": () => ["Infantry", "Ranged"] }); AddMock(cavalryId, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {} }); let currentCavalryPlayer = player; AddMock(cavalryId, IID_Ownership, { "GetOwner": () => currentCavalryPlayer }); AddMock(cavalryId, IID_Garrisonable, { "UnitSize": () => 1, "TotalSize": () => 1, "Garrison": (entity, renamed) => cmpGarrisonHolder.Garrison(cavalryId, renamed), "UnGarrison": () => true }); TS_ASSERT(cmpGarrisonHolder.Garrison(cavalryId)); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 1); // Eject enemy units. currentCavalryPlayer = enemyPlayer; cmpGarrisonHolder.OnGlobalOwnershipChanged({ "entity": cavalryId, "to": enemyPlayer }); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); let oldApplyValueModificationsToEntity = ApplyValueModificationsToEntity; TS_ASSERT(cmpGarrisonHolder.Garrison(siegeEngineId)); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [siegeEngineId]); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => { if (valueName !== "GarrisonHolder/List/_string") return valueName; return HandleTokens(currentValue, "-Siege Trader"); }); cmpGarrisonHolder.OnValueModification({ "component": "GarrisonHolder", "valueNames": ["GarrisonHolder/List/_string"], "entities": [garrisonHolderId] }); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetAllowedClasses().split(/\s+/), ["Infantry+Ranged", "Cavalry", "Trader"]); // The new classes are now cached so we can restore the behavior. Engine.RegisterGlobal("ApplyValueModificationsToEntity", oldApplyValueModificationsToEntity); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); TS_ASSERT(!cmpGarrisonHolder.Garrison(siegeEngineId)); TS_ASSERT(cmpGarrisonHolder.Garrison(traderId)); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Gate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Gate.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Gate.js (revision 27722) @@ -1,101 +1,102 @@ +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/Gate.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Gate.js"); function testBasicBehaviour() { const gate = 10; const own = 11; const passRange = 20; Engine.RegisterGlobal("QueryPlayerIDInterface", () => ({ "GetAllies": () => [1, 2], })); Engine.RegisterGlobal("PlaySound", () => {}); let cmpRangeMgr = AddMock(SYSTEM_ENTITY, IID_RangeManager, { "GetEntityFlagMask": () => {}, "CreateActiveQuery": () => {}, "EnableActiveQuery": () => {}, }); let querySpy = new Spy(cmpRangeMgr, "CreateActiveQuery"); let ownUnitAI = AddMock(own, IID_UnitAI, { "AbleToMove": () => true }); let cmpGate = ConstructComponent(gate, "Gate", { "PassRange": passRange }); let setupSpy = new Spy(cmpGate, "SetupRangeQuery"); let cmpGateObst = AddMock(gate, IID_Obstruction, { "SetDisableBlockMovementPathfinding": () => {}, "GetEntitiesBlockingConstruction": () => [], "GetBlockMovementFlag": () => false, }); AddMock(gate, IID_Ownership, { "GetOwner": () => 1, }); // Test that gates are closed at startup. TS_ASSERT_EQUALS(cmpGate.locked, false); cmpGate.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": 1 }); TS_ASSERT_EQUALS(setupSpy._called, 1); TS_ASSERT_EQUALS(querySpy._callargs[0][2], passRange); TS_ASSERT_UNEVAL_EQUALS(querySpy._callargs[0][3], [1, 2]); TS_ASSERT_EQUALS(cmpGate.opened, false); // Test that they open if units get in range cmpGate.OnRangeUpdate({ "tag": cmpGate.unitsQuery, "added": [own], "removed": [] }); TS_ASSERT_EQUALS(cmpGate.opened, true); TS_ASSERT_UNEVAL_EQUALS(cmpGate.allies, [own]); TS_ASSERT_UNEVAL_EQUALS(cmpGate.ignoreList, []); // Assert that it closes if the unit says it can't move anymore. cmpGate.OnGlobalUnitAbleToMoveChanged({ "entity": own }); TS_ASSERT_EQUALS(cmpGate.opened, false); TS_ASSERT_UNEVAL_EQUALS(cmpGate.ignoreList, [own]); // Assert that it is OK if the entity goes away cmpGate.OnRangeUpdate({ "tag": cmpGate.unitsQuery, "added": [], "removed": [own] }); TS_ASSERT_EQUALS(cmpGate.opened, false); TS_ASSERT_UNEVAL_EQUALS(cmpGate.allies, []); TS_ASSERT_UNEVAL_EQUALS(cmpGate.ignoreList, []); // Lock the gates, try again. cmpGate.LockGate(); TS_ASSERT(cmpGate.IsLocked()); cmpGate.OnRangeUpdate({ "tag": cmpGate.unitsQuery, "added": [own], "removed": [] }); TS_ASSERT_EQUALS(cmpGate.opened, false); TS_ASSERT_UNEVAL_EQUALS(cmpGate.allies, [own]); cmpGate.UnlockGate(); TS_ASSERT_EQUALS(cmpGate.opened, true); cmpGate.LockGate(); TS_ASSERT_EQUALS(cmpGate.opened, false); // Finally, trigger some other handlers to see if things remain correct. setupSpy._reset(); cmpGate.OnOwnershipChanged({ "from": 1, "to": 2 }); TS_ASSERT_EQUALS(setupSpy._called, 1); cmpGate.OnDiplomacyChanged({ "player": 1 }); TS_ASSERT_EQUALS(setupSpy._called, 2); } function testShouldOpen() { let cmpGate = ConstructComponent(5, "Gate", {}); cmpGate.allies = [1, 2, 3, 4]; cmpGate.ignoreList = []; TS_ASSERT_EQUALS(cmpGate.ShouldOpen(), true); cmpGate.ignoreList = [2, 3]; TS_ASSERT_EQUALS(cmpGate.ShouldOpen(), true); cmpGate.ignoreList = [1, 2, 3, 4]; TS_ASSERT_EQUALS(cmpGate.ShouldOpen(), false); cmpGate.allies = []; cmpGate.ignoreList = []; TS_ASSERT_EQUALS(cmpGate.ShouldOpen(), false); } testBasicBehaviour(); testShouldOpen(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Player.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Player.js (revision 27722) @@ -1,101 +1,88 @@ Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], "GetTradableCodes": () => ["food", "metal", "stone", "wood"], "GetBarterableCodes": () => ["food", "metal", "stone", "wood"], "GetResource": () => ({}), "BuildSchema": (type) => { let schema = ""; for (let res of Resources.GetCodes()) schema += "" + "" + "" + "" + ""; return "" + schema + ""; } }; Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Cost.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("Player.js"); var cmpPlayer = ConstructComponent(10, "Player", { "SpyCostMultiplier": 1, "BarterMultiplier": { "Buy": { "wood": 1.0, "stone": 1.0, "metal": 1.0 }, "Sell": { "wood": 1.0, "stone": 1.0, "metal": 1.0 } }, "Formations": { "_string": "" }, }); var playerID = 1; cmpPlayer.SetPlayerID(playerID); TS_ASSERT_EQUALS(cmpPlayer.GetPlayerID(), playerID); TS_ASSERT_EQUALS(cmpPlayer.GetPopulationCount(), 0); TS_ASSERT_EQUALS(cmpPlayer.GetPopulationLimit(), 0); -cmpPlayer.SetDiplomacy([-1, 1, 0, 1, -1]); -TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetAllies(), [1, 3]); -TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetEnemies(), [0, 4]); - -var diplo = cmpPlayer.GetDiplomacy(); -diplo[0] = 1; -TS_ASSERT(cmpPlayer.IsEnemy(0)); - -diplo = [1, 1, 0]; -cmpPlayer.SetDiplomacy(diplo); -diplo[1] = -1; -TS_ASSERT(cmpPlayer.IsAlly(1)); - TS_ASSERT_EQUALS(cmpPlayer.GetSpyCostMultiplier(), 1); TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetBarterMultiplier(), { "buy": { "wood": 1.0, "stone": 1.0, "metal": 1.0 }, "sell": { "wood": 1.0, "stone": 1.0, "metal": 1.0 } }); AddMock(60, IID_Identity, { "GetClassesList": () => {}, "HasClass": (cl) => true }); AddMock(60, IID_Ownership); AddMock(60, IID_Foundation, {}); cmpPlayer.OnGlobalOwnershipChanged({ "entity": 60, "from": INVALID_PLAYER, "to": playerID }); TS_ASSERT(!cmpPlayer.CanBarter()); AddMock(61, IID_Identity, { "GetClassesList": () => {}, "HasClass": (cl) => false }); cmpPlayer.OnGlobalOwnershipChanged({ "entity": 61, "from": INVALID_PLAYER, "to": playerID }); TS_ASSERT(!cmpPlayer.CanBarter()); AddMock(62, IID_Identity, { "GetClassesList": () => {}, "HasClass": (cl) => true }); cmpPlayer.OnGlobalOwnershipChanged({ "entity": 62, "from": INVALID_PLAYER, "to": playerID }); TS_ASSERT(cmpPlayer.CanBarter()); cmpPlayer.OnGlobalOwnershipChanged({ "entity": 62, "from": playerID, "to": INVALID_PLAYER }); TS_ASSERT(!cmpPlayer.CanBarter()); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 27722) @@ -1,548 +1,549 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Position.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); +Engine.LoadComponentScript("interfaces/Diplomacy.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/Turretable.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Formation.js"); Engine.LoadComponentScript("UnitAI.js"); /** * Fairly straightforward test that entity renaming is handled * by unitAI states. These ought to be augmented with integration tests, ideally. */ function TestTargetEntityRenaming(init_state, post_state, setup) { ResetState(); const player_ent = 5; const target_ent = 6; AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": () => {}, "SetTimeout": () => {} }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": () => false }); let unitAI = ConstructComponent(player_ent, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive", "FleeDistance": 10 }); unitAI.OnCreate(); setup(unitAI, player_ent, target_ent); TS_ASSERT_EQUALS(unitAI.GetCurrentState(), init_state); unitAI.OnGlobalEntityRenamed({ "entity": target_ent, "newentity": target_ent + 1 }); TS_ASSERT_EQUALS(unitAI.GetCurrentState(), post_state); } TestTargetEntityRenaming( "INDIVIDUAL.GARRISON.APPROACHING", "INDIVIDUAL.IDLE", (unitAI, player_ent, target_ent) => { unitAI.CanGarrison = (target) => target == target_ent; unitAI.MoveToTargetRange = (target) => target == target_ent; unitAI.AbleToMove = () => true; unitAI.Garrison(target_ent, false); } ); TestTargetEntityRenaming( "INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.REPAIR.REPAIRING", (unitAI, player_ent, target_ent) => { AddMock(player_ent, IID_Builder, { "StartRepairing": () => true, "StopRepairing": () => {} }); QueryBuilderListInterface = () => {}; unitAI.CheckTargetRange = () => true; unitAI.CanRepair = (target) => target == target_ent; unitAI.Repair(target_ent, false, false); } ); TestTargetEntityRenaming( "INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING", (unitAI, player_ent, target_ent) => { PositionHelper.DistanceBetweenEntities = () => 10; unitAI.CheckTargetRangeExplicit = () => false; AddMock(player_ent, IID_UnitMotion, { "MoveToTargetRange": () => true, "GetRunMultiplier": () => 1, "SetSpeedMultiplier": () => {}, "GetAcceleration": () => 1, "StopMoving": () => {} }); unitAI.Flee(target_ent, false); } ); /* Regression test. * Tests the FSM behaviour of a unit when walking as part of a formation, * then exiting the formation. * mode == 0: There is no enemy unit nearby. * mode == 1: There is a live enemy unit nearby. * mode == 2: There is a dead enemy unit nearby. */ function TestFormationExiting(mode) { ResetState(); var playerEntity = 5; var unit = 10; var enemy = 20; var controller = 30; AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": function() { }, "SetTimeout": function() { }, }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) { return 1; }, "EnableActiveQuery": function(id) { }, "ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; }, "DisableActiveQuery": function(id) { }, "GetEntityFlagMask": function(identifier) { }, }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; }, }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": function(id) { return playerEntity; }, "GetNumPlayers": function() { return 2; }, }); - AddMock(playerEntity, IID_Player, { + AddMock(playerEntity, IID_Diplomacy, { "IsAlly": function() { return false; }, "IsEnemy": function() { return true; }, "GetEnemies": function() { return [2]; }, }); var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); AddMock(unit, IID_Identity, { "GetClassesList": function() { return []; }, }); AddMock(unit, IID_Ownership, { "GetOwner": function() { return 1; }, }); AddMock(unit, IID_Position, { "GetTurretParent": function() { return INVALID_ENTITY; }, "GetPosition": function() { return new Vector3D(); }, "GetPosition2D": function() { return new Vector2D(); }, "GetRotation": function() { return { "y": 0 }; }, "IsInWorld": function() { return true; }, }); AddMock(unit, IID_UnitMotion, { "GetWalkSpeed": () => 1, "GetAcceleration": () => 1, "SetSpeedMultiplier": () => {}, "MoveToFormationOffset": (target, x, z) => {}, "MoveToTargetRange": (target, min, max) => true, "SetMemberOfFormation": () => {}, "StopMoving": () => {}, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); AddMock(unit, IID_Vision, { "GetRange": function() { return 10; }, }); AddMock(unit, IID_Attack, { "GetRange": function() { return { "max": 10, "min": 0 }; }, "GetFullAttackRange": function() { return { "max": 40, "min": 0 }; }, "GetBestAttackAgainst": function(t) { return "melee"; }, "GetPreference": function(t) { return 0; }, "GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; }, "CanAttack": function(v) { return true; }, "CompareEntitiesByPreference": function(a, b) { return 0; }, "IsTargetInRange": () => true, "StartAttacking": () => true }); unitAI.OnCreate(); unitAI.SetupAttackRangeQuery(1); if (mode == 1) { AddMock(enemy, IID_Health, { "GetHitpoints": function() { return 10; }, }); AddMock(enemy, IID_UnitAI, { "IsAnimal": () => "false", "IsDangerousAnimal": () => "false" }); } else if (mode == 2) AddMock(enemy, IID_Health, { "GetHitpoints": function() { return 0; }, }); let controllerFormation = ConstructComponent(controller, "Formation", { "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppiness": 0 }); let controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); AddMock(controller, IID_Position, { "JumpTo": function(x, z) { this.x = x; this.z = z; }, "TurnTo": function() {}, "GetTurretParent": function() { return INVALID_ENTITY; }, "GetPosition": function() { return new Vector3D(this.x, 0, this.z); }, "GetPosition2D": function() { return new Vector2D(this.x, this.z); }, "GetRotation": function() { return { "y": 0 }; }, "IsInWorld": function() { return true; }, "MoveOutOfWorld": () => {} }); AddMock(controller, IID_UnitMotion, { "GetWalkSpeed": () => 1, "StopMoving": () => {}, "SetSpeedMultiplier": () => {}, "SetAcceleration": (accel) => {}, "SetPassabilityClassName": (name) => {}, "MoveToPointRange": () => true, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); AddMock(SYSTEM_ENTITY, IID_Pathfinder, { "GetClearance": () => 1, "GetPassabilityClass": () => 16 }); controllerAI.OnCreate(); TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE"); TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); controllerFormation.SetMembers([unit]); controllerAI.Walk(100, 100, false); TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING"); TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING"); controllerFormation.Disband(); unitAI.UnitFsm.ProcessMessage(unitAI, { "type": "Timer" }); if (mode == 0) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); else if (mode == 1) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); else if (mode == 2) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); else TS_FAIL("invalid mode"); } function TestMoveIntoFormationWhileAttacking() { ResetState(); var playerEntity = 5; var controller = 10; var enemy = 20; var unit = 30; var units = []; var unitCount = 8; var unitAIs = []; AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": function() { }, "SetTimeout": function() { }, }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) { return 1; }, "EnableActiveQuery": function(id) { }, "ResetActiveQuery": function(id) { return [enemy]; }, "DisableActiveQuery": function(id) { }, "GetEntityFlagMask": function(identifier) { }, }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; }, }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": function(id) { return playerEntity; }, "GetNumPlayers": function() { return 2; }, }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": (ent, target, min, max) => true }); - AddMock(playerEntity, IID_Player, { + AddMock(playerEntity, IID_Diplomacy, { "IsAlly": function() { return false; }, "IsEnemy": function() { return true; }, "GetEnemies": function() { return [2]; }, }); // create units for (var i = 0; i < unitCount; i++) { units.push(unit + i); var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); AddMock(unit + i, IID_Identity, { "GetClassesList": function() { return []; }, }); AddMock(unit + i, IID_Ownership, { "GetOwner": function() { return 1; }, }); AddMock(unit + i, IID_Position, { "GetTurretParent": function() { return INVALID_ENTITY; }, "GetPosition": function() { return new Vector3D(); }, "GetPosition2D": function() { return new Vector2D(); }, "GetRotation": function() { return { "y": 0 }; }, "IsInWorld": function() { return true; }, }); AddMock(unit + i, IID_UnitMotion, { "GetWalkSpeed": () => 1, "GetAcceleration": () => 1, "SetSpeedMultiplier": () => {}, "MoveToFormationOffset": (target, x, z) => {}, "MoveToTargetRange": (target, min, max) => true, "SetMemberOfFormation": () => {}, "StopMoving": () => {}, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); AddMock(unit + i, IID_Vision, { "GetRange": function() { return 10; }, }); AddMock(unit + i, IID_Attack, { "GetRange": function() { return { "max": 10, "min": 0 }; }, "GetFullAttackRange": function() { return { "max": 40, "min": 0 }; }, "GetBestAttackAgainst": function(t) { return "melee"; }, "GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; }, "CanAttack": function(v) { return true; }, "CompareEntitiesByPreference": function(a, b) { return 0; }, "IsTargetInRange": () => true, "StartAttacking": () => true, "StopAttacking": () => {} }); unitAI.OnCreate(); unitAI.SetupAttackRangeQuery(1); unitAIs.push(unitAI); } // create enemy AddMock(enemy, IID_Health, { "GetHitpoints": function() { return 40; }, }); let controllerFormation = ConstructComponent(controller, "Formation", { "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppiness": 0 }); let controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); AddMock(controller, IID_Position, { "GetTurretParent": () => INVALID_ENTITY, "JumpTo": function(x, z) { this.x = x; this.z = z; }, "TurnTo": function() {}, "GetPosition": function(){ return new Vector3D(this.x, 0, this.z); }, "GetPosition2D": function(){ return new Vector2D(this.x, this.z); }, "GetRotation": () => ({ "y": 0 }), "IsInWorld": () => true, "MoveOutOfWorld": () => {}, }); AddMock(controller, IID_UnitMotion, { "GetWalkSpeed": () => 1, "SetSpeedMultiplier": (speed) => {}, "SetAcceleration": (accel) => {}, "SetPassabilityClassName": (name) => {}, "MoveToPointRange": (x, z, minRange, maxRange) => {}, "StopMoving": () => {}, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); AddMock(SYSTEM_ENTITY, IID_Pathfinder, { "GetClearance": () => 1, "GetPassabilityClass": () => 16 }); AddMock(controller, IID_Attack, { "GetRange": function() { return { "max": 10, "min": 0 }; }, "CanAttackAsFormation": function() { return false; }, }); controllerAI.OnCreate(); controllerFormation.SetMembers(units); controllerAI.Attack(enemy, []); for (let ent of unitAIs) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); controllerAI.MoveIntoFormation({ "name": "Circle" }); // let all units be in position for (let ent of unitAIs) controllerFormation.SetFinishedEntity(ent); for (let ent of unitAIs) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); controllerFormation.Disband(); } TestFormationExiting(0); TestFormationExiting(1); TestFormationExiting(2); TestMoveIntoFormationWhileAttacking(); function TestWalkAndFightTargets() { const ent = 10; let unitAI = ConstructComponent(ent, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive", "FleeDistance": 10 }); unitAI.OnCreate(); unitAI.losAttackRangeQuery = true; // The result is stored here let result; unitAI.PushOrderFront = function(type, order) { if (type === "Attack" && order?.target) result = order.target; }; // Create some targets. AddMock(ent+1, IID_UnitAI, { "IsAnimal": () => true, "IsDangerousAnimal": () => false }); AddMock(ent+2, IID_Ownership, { "GetOwner": () => 2 }); AddMock(ent+3, IID_Ownership, { "GetOwner": () => 2 }); AddMock(ent+4, IID_Ownership, { "GetOwner": () => 2 }); AddMock(ent+5, IID_Ownership, { "GetOwner": () => 2 }); AddMock(ent+6, IID_Ownership, { "GetOwner": () => 2 }); AddMock(ent+7, IID_Ownership, { "GetOwner": () => 2 }); unitAI.CanAttack = function(target) { return target !== ent+2 && target !== ent+7; }; AddMock(ent, IID_Attack, { "GetPreference": (target) => ({ [ent+4]: 0, [ent+5]: 1, [ent+6]: 2, [ent+7]: 0 }?.[target]) }); let runTest = function(ents, res) { result = undefined; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ResetActiveQuery": () => ents }); TS_ASSERT_EQUALS(unitAI.FindWalkAndFightTargets(), !!res); TS_ASSERT_EQUALS(result, res); }; // No entities. runTest([]); // Entities that cannot be attacked. runTest([ent+1, ent+2, ent+7]); // No preference, one attackable entity. runTest([ent+1, ent+2, ent+3], ent+3); // Check preferences. runTest([ent+1, ent+2, ent+3, ent+4], ent+4); runTest([ent+1, ent+2, ent+3, ent+4, ent+5], ent+4); runTest([ent+1, ent+2, ent+6, ent+3, ent+4, ent+5], ent+4); runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+4, ent+5], ent+4); runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+5], ent+5); runTest([ent+1, ent+2, ent+7, ent+6, ent+3], ent+6); runTest([ent+1, ent+2, ent+7, ent+3], ent+3); } TestWalkAndFightTargets(); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 27721) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 27722) @@ -1,314 +1,296 @@ /** * Used to create player entities prior to reading the rest of a map, * all other initialization must be done after loading map (terrain/entities). * Be VERY careful in using other components here, as they may not be properly initialised yet. * settings is the object containing settings for this map. * newPlayers if true will remove old player entities or add new ones until * the new number of player entities is obtained * (used when loading a map or when Atlas changes the number of players). */ function LoadPlayerSettings(settings, newPlayers) { const playerDefaults = Engine.ReadJSONFile("simulation/data/settings/player_defaults.json").PlayerData; const playerData = settings.PlayerData; if (!playerData) warn("Player.js: Setup has no player data - using defaults."); const getPlayerSetting = (idx, property) => { if (playerData && playerData[idx] && (property in playerData[idx])) return playerData[idx][property]; if (playerDefaults && playerDefaults[idx] && (property in playerDefaults[idx])) return playerDefaults[idx][property]; return undefined; }; // Add gaia to simplify iteration // (if gaia is not already the first civ such as when called from Atlas' ActorViewer) if (playerData && playerData[0] && (!playerData[0].Civ || playerData[0].Civ != "gaia")) playerData.unshift(null); if (playerData && !playerData.some(v => v && !!v.AI)) Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface).Disable(); const cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let numPlayers = cmpPlayerManager.GetNumPlayers(); // Remove existing players or add new ones if (newPlayers) { const settingsNumPlayers = playerData?.length ?? playerDefaults.length; while (numPlayers < settingsNumPlayers) cmpPlayerManager.AddPlayer(GetPlayerTemplateName(getPlayerSetting(numPlayers++, "Civ"))); for (; numPlayers > settingsNumPlayers; numPlayers--) cmpPlayerManager.RemoveLastPlayer(); } // Even when no new player, we must check the template compatibility as player templates are civ dependent. const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); for (let i = 0; i < numPlayers; ++i) { const template = GetPlayerTemplateName(getPlayerSetting(i, "Civ")); const entID = cmpPlayerManager.GetPlayerByID(i); if (cmpTemplateManager.GetCurrentTemplateName(entID) !== template) cmpPlayerManager.ReplacePlayerTemplate(i, template); } for (let i = 0; i < numPlayers; ++i) { QueryPlayerIDInterface(i, IID_Identity).SetName(getPlayerSetting(i, "Name")); const color = getPlayerSetting(i, "Color"); const cmpPlayer = QueryPlayerIDInterface(i); cmpPlayer.SetColor(color.r, color.g, color.b); // Special case for gaia if (i == 0) continue; // PopulationLimit { const maxPopulation = settings.PlayerData[i].PopulationLimit !== undefined ? settings.PlayerData[i].PopulationLimit : settings.PopulationCap !== undefined ? settings.PopulationCap : playerDefaults[i].PopulationLimit !== undefined ? playerDefaults[i].PopulationLimit : undefined; if (maxPopulation !== undefined) cmpPlayer.SetMaxPopulation(maxPopulation); } // StartingResources if (settings.PlayerData[i].Resources !== undefined) cmpPlayer.SetResourceCounts(settings.PlayerData[i].Resources); else if (settings.StartingResources) { let resourceCounts = cmpPlayer.GetResourceCounts(); let newResourceCounts = {}; for (let resources in resourceCounts) newResourceCounts[resources] = settings.StartingResources; cmpPlayer.SetResourceCounts(newResourceCounts); } else if (playerDefaults[i].Resources !== undefined) cmpPlayer.SetResourceCounts(playerDefaults[i].Resources); if (settings.DisableSpies) { cmpPlayer.AddDisabledTechnology("unlock_spies"); cmpPlayer.AddDisabledTemplate("special/spy"); } // If diplomacy explicitly defined, use that; otherwise use teams. const diplomacy = getPlayerSetting(i, "Diplomacy"); if (diplomacy !== undefined) - cmpPlayer.SetDiplomacy(diplomacy); + QueryPlayerIDInterface(i, IID_Diplomacy).SetDiplomacy(diplomacy); else - cmpPlayer.SetTeam(getPlayerSetting(i, "Team") ?? -1); + QueryPlayerIDInterface(i, IID_Diplomacy).ChangeTeam(getPlayerSetting(i, "Team") ?? -1); const formations = getPlayerSetting(i, "Formations"); if (formations) cmpPlayer.SetFormations(formations); const startCam = getPlayerSetting(i, "StartingCamera"); if (startCam) cmpPlayer.SetStartingCamera(startCam.Position, startCam.Rotation); } - // NOTE: We need to do the team locking here, as otherwise - // SetTeam can't ally the players. + // NOTE: We need to do the team locking here, as + // otherwise we can't ally the players above. if (settings.LockTeams) for (let i = 0; i < numPlayers; ++i) - QueryPlayerIDInterface(i).SetLockTeams(true); + QueryPlayerIDInterface(i, IID_Diplomacy).LockTeam(); } function GetPlayerTemplateName(civ) { return "special/players/" + civ; } /** * @param id An entity's ID * @returns The entity ID of the owner player (not his player ID) or ent if ent is a player entity. */ function QueryOwnerEntityID(ent) { let cmpPlayer = Engine.QueryInterface(ent, IID_Player); if (cmpPlayer) return ent; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; let owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return null; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager) return null; return cmpPlayerManager.GetPlayerByID(owner); } /** * Similar to Engine.QueryInterface but applies to the player entity * that owns the given entity. * iid is typically IID_Player. */ function QueryOwnerInterface(ent, iid = IID_Player) { var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; var owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return null; return QueryPlayerIDInterface(owner, iid); } /** * Similar to Engine.QueryInterface but applies to the player entity * with the given ID number. * iid is typically IID_Player. */ function QueryPlayerIDInterface(id, iid = IID_Player) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerEnt = cmpPlayerManager.GetPlayerByID(id); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } /** * Similar to Engine.QueryInterface but first checks if the entity * mirages the interface. */ function QueryMiragedInterface(ent, iid) { let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage && !cmpMirage.Mirages(iid)) return null; else if (!cmpMirage) return Engine.QueryInterface(ent, iid); return cmpMirage.Get(iid); } /** * Similar to Engine.QueryInterface, but checks for all interfaces * implementing a builder list (currently Foundation and Repairable) * TODO Foundation and Repairable could both implement a BuilderList component */ function QueryBuilderListInterface(ent) { return Engine.QueryInterface(ent, IID_Foundation) || Engine.QueryInterface(ent, IID_Repairable); } /** * Returns true if the entity 'target' is owned by an ally of * the owner of 'entity'. */ function IsOwnedByAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsAlly"); } function IsOwnedByMutualAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsMutualAlly"); } function IsOwnedByEntityHelper(entity, target, check) { - // Figure out which player controls us - let owner = 0; - let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); - if (cmpOwnership) - owner = cmpOwnership.GetOwner(); - - // Figure out which player controls the target entity - let targetOwner = 0; - let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); - if (cmpOwnershipTarget) - targetOwner = cmpOwnershipTarget.GetOwner(); - - let cmpPlayer = QueryPlayerIDInterface(owner); - - return cmpPlayer && cmpPlayer[check](targetOwner); + const owner = Engine.QueryInterface(entity, IID_Ownership)?.GetOwner() || 0; + return IsOwnedByHelper(owner, target, check); } /** * Returns true if the entity 'target' is owned by player */ function IsOwnedByPlayer(player, target) { var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); return cmpOwnershipTarget && player == cmpOwnershipTarget.GetOwner(); } function IsOwnedByGaia(target) { return IsOwnedByPlayer(0, target); } /** * Returns true if the entity 'target' is owned by an ally of player */ function IsOwnedByAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsAlly"); } function IsOwnedByMutualAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsMutualAlly"); } function IsOwnedByNeutralOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsNeutral"); } function IsOwnedByEnemyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsEnemy"); } function IsOwnedByHelper(player, target, check) { - let targetOwner = 0; - let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); - if (cmpOwnershipTarget) - targetOwner = cmpOwnershipTarget.GetOwner(); - - let cmpPlayer = QueryPlayerIDInterface(player); - - return cmpPlayer && cmpPlayer[check](targetOwner); + const targetOwner = Engine.QueryInterface(target, IID_Ownership)?.GetOwner() || 0; + const cmpDiplomacy = QueryPlayerIDInterface(player, IID_Diplomacy); + return cmpDiplomacy && cmpDiplomacy[check](targetOwner); } Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings); Engine.RegisterGlobal("QueryOwnerEntityID", QueryOwnerEntityID); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface); Engine.RegisterGlobal("QueryMiragedInterface", QueryMiragedInterface); Engine.RegisterGlobal("QueryBuilderListInterface", QueryBuilderListInterface); Engine.RegisterGlobal("IsOwnedByAllyOfEntity", IsOwnedByAllyOfEntity); Engine.RegisterGlobal("IsOwnedByMutualAllyOfEntity", IsOwnedByMutualAllyOfEntity); Engine.RegisterGlobal("IsOwnedByPlayer", IsOwnedByPlayer); Engine.RegisterGlobal("IsOwnedByGaia", IsOwnedByGaia); Engine.RegisterGlobal("IsOwnedByAllyOfPlayer", IsOwnedByAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByMutualAllyOfPlayer", IsOwnedByMutualAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByNeutralOfPlayer", IsOwnedByNeutralOfPlayer); Engine.RegisterGlobal("IsOwnedByEnemyOfPlayer", IsOwnedByEnemyOfPlayer);