Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 17407) +++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 17408) @@ -1,938 +1,941 @@ /** * List of different actions units can execute, * this is mostly used to determine which actions can be executed * * "execute" is meant to send the command to the engine * * The next functions will always return false * in case you have to continue to seek * (i.e. look at the next entity for getActionInfo, the next * possible action for the actionCheck ...) * They will return an object when the searching is finished * * "getActionInfo" is used to determine if the action is possible, * and also give visual feedback to the user (tooltips, cursors, ...) * * "preSelectedActionCheck" is used to select actions when the gui buttons * were used to set them, but still require a target (like the guard button) * * "hotkeyActionCheck" is used to check the possibility of actions when * a hotkey is pressed * * "actionCheck" is used to check the possibilty of actions without specific * command. For that, the specificness variable is used * * "specificness" is used to determine how specific an action is, * The lower the number, the more specific an action is, and the bigger * the chance of selecting that action when multiple actions are possible */ var unitActions = { "move": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { return {"possible": true}; }, "actionCheck": function(target, selection) { // Work out whether at least part of the selection have UnitAI var haveUnitAI = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI; }); if (haveUnitAI && getActionInfo("move", target).possible) return {"type": "move"}; return false; }, "specificness": 12, }, "attack-move": { "execute": function(target, action, selection, queued) { if (Engine.HotkeyIsPressed("session.attackmoveUnit")) var targetClasses = { "attack": ["Unit"] }; else var targetClasses = { "attack": ["Unit", "Structure"] }; Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { return {"possible": true}; }, "hotkeyActionCheck": function(target, selection) { // Work out whether at least part of the selection have UnitAI var haveUnitAI = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI; }); if (haveUnitAI && Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible) return {"type": "attack-move", "cursor": "action-attack-move"}; return false; }, "specificness": 30, }, "capture": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "allowCapture": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.attack || !targetState.hitpoints) return false; return {"possible": Engine.GuiInterfaceCall("CanCapture", {"entity": entState.id, "target": targetState.id})}; }, "actionCheck": function(target) { if (getActionInfo("capture", target).possible) return {"type": "capture", "cursor": "action-capture", "target": target}; return false; }, "specificness": 9, }, "attack": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued, "allowCapture": false}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.attack || !targetState.hitpoints) return false; return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})}; }, "hotkeyActionCheck": function(target) { if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible) return {"type": "attack", "cursor": "action-attack", "target": target}; return false; }, "actionCheck": function(target) { if (getActionInfo("attack", target).possible) return {"type": "attack", "cursor": "action-attack", "target": target}; return false; }, "specificness": 10, }, "heal": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.healer) return false; if (!hasClass(targetState, "Unit") || !targetState.needsHeal) return false; if (!playerCheck(entState, targetState, ["Player", "Ally"])) return false; // Healers can't heal themselves. if (entState.id == targetState.id) return false; var unhealableClasses = entState.healer.unhealableClasses; if (MatchesClassList(targetState.identity.classes, unhealableClasses)) return false; var healableClasses = entState.healer.healableClasses; if (!MatchesClassList(targetState.identity.classes, healableClasses)) return false; return {"possible": true}; }, "actionCheck": function(target) { if (getActionInfo("heal", target).possible) return {"type": "heal", "cursor": "action-heal", "target": target}; return false; }, "specificness": 7, }, "build": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (targetState.foundation && entState.builder && playerCheck(entState, targetState, ["Player", "Ally"])) return {"possible": true}; return false; }, "actionCheck": function(target) { if (getActionInfo("build", target).possible) return {"type": "build", "cursor": "action-build", "target": target}; return false; }, "specificness": 3, }, "repair": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (entState.builder && targetState.needsRepair && playerCheck(entState, targetState, ["Player", "Ally"])) return {"possible": true}; return false; }, "preSelectedActionCheck" : function(target) { if (preSelectedAction != ACTION_REPAIR) return false; if (getActionInfo("repair", target).possible) return {"type": "repair", "cursor": "action-repair", "target": target}; return {"type": "none", "cursor": "action-repair-disabled", "target": null}; }, "actionCheck": function(target) { if (getActionInfo("repair", target).possible) return {"type": "build", "cursor": "action-repair", "target": target}; return false; }, "specificness": 11, }, "gather": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState.resourceSupply) return false; var resource = findGatherType(entState, targetState.resourceSupply); if (resource) return {"possible": true, "cursor": "action-gather-" + resource}; return false; }, "actionCheck": function(target) { var actionInfo = getActionInfo("gather", target); if (!actionInfo.possible) return false; return {"type": "gather", "cursor": actionInfo.cursor, "target": target}; }, "specificness": 1, }, "returnresource": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState.resourceDropsite) return false; if (!playerCheck(entState, targetState, ["Player"])) return false; if (!entState.resourceCarrying || !entState.resourceCarrying.length) return false; var carriedType = entState.resourceCarrying[0].type; if (targetState.resourceDropsite.types.indexOf(carriedType) == -1) return false; return {"possible": true, "cursor": "action-return-" + carriedType}; }, "actionCheck": function(target) { var actionInfo = getActionInfo("returnresource", target); if (!actionInfo.possible) return false; return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target}; }, "specificness": 2, }, "setup-trade-route": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target, "source": null, "route": null, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (targetState.foundation || !entState.trader) return false; if (!playerCheck(entState, targetState, ["Player", "Ally"])) return false; if (!(hasClass(entState, "Organic") && hasClass(targetState, "Market")) && !(hasClass(entState, "Ship") && hasClass(targetState, "NavalMarket"))) return false; var tradingData = {"trader": entState.id, "target": targetState.id}; var tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", tradingData); if (!tradingDetails) return false; var tooltip; switch (tradingDetails.type) { case "is first": tooltip = translate("Origin trade market."); if (tradingDetails.hasBothMarkets) tooltip += "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(tradingDetails.gain) }); else tooltip += "\n" + translate("Right-click on another market to set it as a destination trade market."); break; case "is second": tooltip = translate("Destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(tradingDetails.gain) }); break; case "set first": tooltip = translate("Right-click to set as origin trade market"); break; case "set second": if (tradingDetails.gain.traderGain == 0) // markets too close return false; tooltip = translate("Right-click to set as destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(tradingDetails.gain) }); break; } return {"possible": true, "tooltip": tooltip}; }, "actionCheck": function(target) { var actionInfo = getActionInfo("setup-trade-route", target); if (!actionInfo.possible) return false; return {"type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target}; }, "specificness": 0, }, "garrison": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!hasClass(entState, "Unit") || !targetState.garrisonHolder) return false; if (!playerCheck(entState, targetState, ["Player", "Ally"])) return false; var tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount, capacity: targetState.garrisonHolder.capacity }); var extraCount = 0; if (entState.garrisonHolder) extraCount += entState.garrisonHolder.garrisonedEntitiesCount; if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity) tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) return {"possible": true, "tooltip": tooltip}; return false; }, "preSelectedActionCheck": function(target) { if (preSelectedAction != ACTION_GARRISON) return false; var actionInfo = getActionInfo("garrison", target); if (actionInfo.possible) return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target}; return {"type": "none", "cursor": "action-garrison-disabled", "target": null}; }, "hotkeyActionCheck": function(target) { var actionInfo = getActionInfo("garrison", target); if (Engine.HotkeyIsPressed("session.garrison") && actionInfo.possible) return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target}; return false; }, "specificness": 20, }, "guard": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "guard", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState.guard) return false; if (!playerCheck(entState, targetState, ["Player", "Ally"])) return false; if (!entState.unitAI || !entState.unitAI.canGuard) return false; if (targetState.unitAI && targetState.unitAI.isGuarding) return false; return {"possible": true}; }, "preSelectedActionCheck" : function(target) { if (preSelectedAction != ACTION_GUARD) return false; if (getActionInfo("guard", target).possible) return {"type": "guard", "cursor": "action-guard", "target": target}; return {"type": "none", "cursor": "action-guard-disabled", "target": null}; }, "hotkeyActionCheck": function(target) { if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("guard", target).possible) return {"type": "guard", "cursor": "action-guard", "target": target}; return false; }, "specificness": 40, }, "remove-guard": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "remove-guard", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] }); return true; }, "hotkeyActionCheck": function(target, selection) { if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("remove-guard", target).possible) { var isGuarding = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI && entState.unitAI.isGuarding; }); if (isGuarding) return {"type": "remove-guard", "cursor": "action-remove-guard"}; } return false; }, "specificness": 41, }, "set-rallypoint": { "execute": function(target, action, selection, queued) { // if there is a position set in the action then use this so that when setting a // rally point on an entity it is centered on that entity if (action.position) target = action.position; Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z, "data": action.data, "queued": queued}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z, "queued": queued }); return true; }, "getActionInfo": function(entState, targetState) { var tooltip; // default to walking there (or attack-walking if hotkey pressed) var data = {command: "walk"}; var cursor = ""; if (Engine.HotkeyIsPressed("session.attackmove")) { if (Engine.HotkeyIsPressed("session.attackmoveUnit")) var targetClasses = { "attack": ["Unit"] }; else var targetClasses = { "attack": ["Unit", "Structure"] }; data.command = "attack-walk"; data.targetClasses = targetClasses; cursor = "action-attack-move"; } if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "Ally"])) { data.command = "garrison"; data.target = targetState.id; cursor = "action-garrison"; tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount, capacity: targetState.garrisonHolder.capacity }); if (targetState.garrisonHolder.garrisonedEntitiesCount >= targetState.garrisonHolder.capacity) tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; } else if (targetState.resourceSupply) { var resourceType = targetState.resourceSupply.type; if (resourceType.generic == "treasure") cursor = "action-gather-" + resourceType.generic; else cursor = "action-gather-" + resourceType.specific; data.command = "gather"; data.resourceType = resourceType; data.resourceTemplate = targetState.template; } else if (hasClass(entState, "Market") && hasClass(targetState, "Market") && entState.id != targetState.id && (!hasClass(entState, "NavalMarket") || hasClass(targetState, "NavalMarket")) && !playerCheck(entState, targetState, ["Enemy"])) { // Find a trader (if any) that this building can produce. var trader; if (entState.production && entState.production.entities.length) for (var i = 0; i < entState.production.entities.length; ++i) if ((trader = GetTemplateData(entState.production.entities[i]).trader)) break; var traderData = { "firstMarket": entState.id, "secondMarket": targetState.id, "template": trader }; var gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData); if (gain && gain.traderGain) { data.command = "trade"; data.target = traderData.secondMarket; data.source = traderData.firstMarket; cursor = "action-setup-trade-route"; tooltip = translate("Right-click to establish a default route for new traders."); if (trader) tooltip += "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(gain) }); else // Foundation or cannot produce traders tooltip += "\n" + sprintf(translate("Expected gain: %(gain)s"), { gain: getTradingTooltip(gain) }); } } else if (targetState.foundation && playerCheck(entState, targetState, ["Ally"])) { data.command = "build"; data.target = targetState.id; cursor = "action-build"; } else if (targetState.needsRepair && playerCheck(entState, targetState, ["Ally"])) { data.command = "repair"; data.target = targetState.id; cursor = "action-repair"; } else if (playerCheck(entState, targetState, ["Enemy"])) { data.target = targetState.id; data.command = "attack"; cursor = "action-attack"; } // Don't allow the rally point to be set on any of the currently selected entities (used for unset) // except if the autorallypoint hotkey is pressed and the target can produce entities if (!Engine.HotkeyIsPressed("session.autorallypoint") || !targetState.production || !targetState.production.entities.length) { for each (var ent in g_Selection.selected) if (targetState.id === ent) return false; } return {"possible": true, "data": data, "position": targetState.position, "cursor": cursor, "tooltip": tooltip}; }, "actionCheck": function(target, selection) { // Work out whether at least part of the selection have UnitAI var haveUnitAI = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI; }); if (haveUnitAI) return false; // Work out whether at least part the selection have rally points // while none have UnitAI var haveRallyPoints = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && ("rallyPoint" in entState) && entState.rallyPoint; }); if (!haveRallyPoints) return false; var actionInfo = getActionInfo("set-rallypoint", target); if (!actionInfo.possible) return false; return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position}; }, "specificness": 6, }, "unset-rallypoint": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection}); // Remove displayed rally point Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": [] }); return true; }, "getActionInfo": function(entState, targetState) { if (entState.id != targetState.id) return false; if (!entState.rallyPoint || !entState.rallyPoint.position) return false; return {"possible": true}; }, "actionCheck": function(target, selection) { // Work out whether at least part of the selection have UnitAI var haveUnitAI = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI; }); // Work out whether at least part the selection have rally points // while none have UnitAI var haveRallyPoints = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && ("rallyPoint" in entState) && entState.rallyPoint; }); if (!haveUnitAI && haveRallyPoints && getActionInfo("unset-rallypoint", target).possible) return {"type": "unset-rallypoint", "cursor": "action-unset-rally"}; return false; }, "specificness": 11, }, "none": { "execute": function(target, action, selection, queued) { return true; }, "specificness": 100, }, }; /** * Info and actions for the entity commands * Currently displayed in the bottom of the central panel */ var g_EntityCommands = { // Unload "unload-all": { "getInfo": function(entState) { if (!entState.garrisonHolder) return false; var count = 0; for each (var ent in g_Selection.selected) { var state = GetEntityState(ent); if (state.garrisonHolder) count += state.garrisonHolder.entities.length; } return { "tooltip": translate("Unload All"), "icon": "garrison-out.png", "count": count, }; }, "execute": function(entState) { unloadAll(); }, }, // Delete "delete": { "getInfo": function(entState) { + if (!entState.canDelete) + return false; + if (entState.mirage) return { "tooltip": translate("You cannot destroy this entity because it is in the fog-of-war"), "icon": "kill_small_disabled.png" }; if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2) return { "tooltip": translate("You cannot destroy this entity as you own less than half the capture points"), "icon": "kill_small_disabled.png" }; return { "tooltip": translate("Delete"), "icon": "kill_small.png" }; }, "execute": function(entState) { if (entState.mirage) return; if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2) return; var selection = g_Selection.toList(); if (selection.length < 1) return; if (!entState.resourceSupply || !entState.resourceSupply.killBeforeGather) openDeleteDialog(selection); }, }, // Stop "stop": { "getInfo": function(entState) { if (!entState.unitAI) return false; return { "tooltip": translate("Stop"), "icon": "stop.png" }; }, "execute": function(entState) { var selection = g_Selection.toList(); if (selection.length > 0) stopUnits(selection); }, }, // Garrison "garrison": { "getInfo": function(entState) { if (!entState.unitAI || entState.turretParent) return false; return { "tooltip": translate("Garrison"), "icon": "garrison.png" }; }, "execute": function(entState) { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; }, }, // Ungarrison "unload": { "getInfo": function(entState) { if (!entState.unitAI || !entState.turretParent) return false; var p = GetEntityState(entState.turretParent); if (!p.garrisonHolder || p.garrisonHolder.entities.indexOf(entState.id) == -1) return false; return { "tooltip": translate("Unload"), "icon": "garrison-out.png" }; }, "execute": function(entState) { unloadSelection(); }, }, // Repair "repair": { "getInfo": function(entState) { if (!entState.builder) return false; return { "tooltip": translate("Repair"), "icon": "repair.png" }; }, "execute": function(entState) { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; }, }, // Focus on rally point "focus-rally": { "getInfo": function(entState) { if (!entState.rallyPoint) return false; return { "tooltip": translate("Focus on Rally Point"), "icon": "focus-rally.png" }; }, "execute": function(entState) { var focusTarget = null; if (entState.rallyPoint && entState.rallyPoint.position) focusTarget = entState.rallyPoint.position; else if (entState.position) focusTarget = entState.position; if (focusTarget) Engine.CameraMoveTo(focusTarget.x, focusTarget.z); }, }, // Back to work "back-to-work": { "getInfo": function(entState) { if (!entState.unitAI || !entState.unitAI.hasWorkOrders) return false; return { "tooltip": translate("Back to Work"), "icon": "production.png" }; }, "execute": function(entState) { backToWork(); }, }, // Guard "add-guard": { "getInfo": function(entState) { if (!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding) return false; return { "tooltip": translate("Guard"), "icon": "add-guard.png" }; }, "execute": function(entState) { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GUARD; }, }, // Remove guard "remove-guard": { "getInfo": function(entState) { if (!entState.unitAI || !entState.unitAI.isGuarding) return false; return { "tooltip": translate("Remove guard"), "icon": "remove-guard.png" }; }, "execute": function(entState) { removeGuard(); }, }, // Trading "select-trading-goods": { "getInfo": function(entState) { if (!hasClass(entState, "Market")) return false; return { "tooltip": translate("Select trading goods"), "icon": "economics.png" }; }, "execute": function(entState) { toggleTrade(); }, }, }; var g_AllyEntityCommands = { // Unload "unload-all": { "getInfo": function(entState) { if (!entState.garrisonHolder) return false; var count = 0; for each (var ent in g_Selection.selected) { var selectedEntState = GetEntityState(ent); if (selectedEntState.garrisonHolder) { var player = Engine.GetPlayerID(); for (var entity of selectedEntState.garrisonHolder.entities) { var state = GetEntityState(entity); if (state.player == player) count++; } } } return { "tooltip": translate("Unload All"), "icon": "garrison-out.png", "count": count, }; }, "execute": function(entState) { unloadAllByOwner(); }, }, }; function playerCheck(entState, targetState, validPlayers) { var playerState = GetSimState().players[entState.player]; for (var player of validPlayers) { if (player == "Gaia" && targetState.player == 0) return true; if (player == "Player" && targetState.player == entState.player) return true; if (playerState["is"+player] && playerState["is"+player][targetState.player]) return true; } return false; } Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 17407) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 17408) @@ -1,1926 +1,1927 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronised 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(); }; /* * 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(player) { var ret = { "players": [] }; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var numPlayers = cmpPlayerManager.GetNumPlayers(); for (var i = 0; i < numPlayers; ++i) { var playerEnt = cmpPlayerManager.GetPlayerByID(i); var cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); // Work out what phase we are in var phase = ""; var 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"; } // store player ally/neutral/enemy data as arrays var allies = []; var mutualAllies = []; var neutrals = []; var enemies = []; for (var 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); } var playerData = { "name": cmpPlayer.GetName(), "civ": cmpPlayer.GetCiv(), "color": cmpPlayer.GetColor(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "heroes": cmpPlayer.GetHeroes(), "resourceCounts": cmpPlayer.GetResourceCounts(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedResearch() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null }; ret.players.push(playerData); } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = 4 * cmpTerrain.GetTilesPerSide(); // Add timeElapsed var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); // Add ceasefire info var cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (cmpCeasefireManager) { ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive(); ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0; } // Add the game type var cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.gameType = cmpEndGameManager.GetGameType(); // Add bartering prices var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); ret.barterPrices = cmpBarter.GetPrices(); // Add basic statistics to each player for (var i = 0; i < numPlayers; ++i) { var playerEnt = cmpPlayerManager.GetPlayerByID(i); var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, 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(player) { // Get basic simulation info var ret = this.GetSimulationState(); // Add statistics to each player var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerManager.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerManager.GetPlayerByID(i); var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics(); } return ret; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); else return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function(player) { 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) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // All units must have a template; if not then it's a nonexistent entity id var template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; var ret = { "id": ent, "template": template, "alertRaiser": null, "builder": null, "identity": null, "fogging": null, "foundation": null, "garrisonHolder": null, "gate": null, "guard": null, "mirage": null, "pack": null, "player": -1, "position": null, "production": null, "rallyPoint": null, "resourceCarrying": null, "rotation": null, "trader": null, "unitAI": null, "visibility": null, }; var cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) { ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "visibleClasses": cmpIdentity.GetVisibleClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName() }; } var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { ret.position = cmpPosition.GetPosition(); ret.rotation = cmpPosition.GetRotation(); } var cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = Math.ceil(cmpHealth.GetHitpoints()); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); ret.needsHeal = !cmpHealth.IsUnhealable(); + ret.canDelete = !cmpHealth.IsUndeletable(); } var cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable) { ret.capturePoints = cmpCapturable.GetCapturePoints(); ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); } var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; var cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) { ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress(), }; } var cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) { ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "queue": cmpProductionQueue.GetQueue(), }; } var cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) { ret.trader = { "goods": cmpTrader.GetGoods(), "requiredGoods": cmpTrader.GetRequiredGoods() }; } var cmpFogging = Engine.QueryInterface(ent, IID_Fogging); if (cmpFogging) { if (cmpFogging.IsMiraged(player)) ret.fogging = {"mirage": cmpFogging.GetMirage(player)}; else ret.fogging = {"mirage": null}; } var cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) { ret.foundation = { "progress": cmpFoundation.GetBuildPercentage(), "numBuilders": cmpFoundation.GetNumBuilders() }; } var cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) { ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() }; } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) { ret.player = cmpOwnership.GetOwner(); } var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { ret.rallyPoint = {'position': cmpRallyPoint.GetPositions()[0]}; // undefined or {x,z} object } var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) { ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() }; } var 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(), "possibleStances": cmpUnitAI.GetPossibleStances(), "isIdle":cmpUnitAI.IsIdle(), }; // Add some information needed for ungarrisoning if (cmpUnitAI.IsGarrisoned() && ret.player !== undefined) ret.template = "p" + ret.player + "&" + ret.template; } var cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) { ret.guard = { "entities": cmpGuard.GetEntities(), }; } var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); } var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) { ret.gate = { "locked": cmpGate.IsLocked(), }; } var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) { ret.alertRaiser = { "level": cmpAlertRaiser.GetLevel(), "canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(), "hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(), }; } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); return ret; }; /** * Get additionnal entity info, rarely used in the gui */ GuiInterface.prototype.GetExtendedEntityState = function(player, ent) { var ret = { "armour": null, "attack": null, "barterMarket": null, "buildingAI": null, "healer": null, "obstruction": null, "turretParent":null, "promotion": null, "resourceDropsite": null, "resourceGatherRates": null, "resourceSupply": null, }; var cmpMirage = Engine.QueryInterface(ent, IID_Mirage); var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); var cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { var types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (var type of types) { ret.attack[type] = cmpAttack.GetAttackStrengths(type); var range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; var timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { // not a ranged attack, set some defaults ret.attack[type].elevationBonus = 0; ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } ret.attack[type].elevationBonus = range.elevationBonus; var cmpPosition = Engine.QueryInterface(ent, IID_Position); var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld()) { // For units, take the rage in front of it, no spread. So angle = 0 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0); } else if(cmpPosition && cmpPosition.IsInWorld()) { // For buildings, take the average elevation around it. So angle = 2*pi ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI); } else { // not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } } var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); if (cmpArmour) { ret.armour = cmpArmour.GetArmourStrengths(); } var cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras) { ret.auras = cmpAuras.GetDescriptions(); } var cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) { ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; } var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (cmpObstruction) { ret.obstruction = { "controlGroup": cmpObstruction.GetControlGroup(), "controlGroup2": cmpObstruction.GetControlGroup2(), }; } var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); var 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() }; } var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) { ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes() }; } var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) { ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; } var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket")) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); ret.barterMarket = { "prices": cmpBarter.GetPrices() }; } var cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) { ret.healer = { "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses(), }; } return ret; }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var rot = {x:0, y:0, z:0}; var pos = {x:cmd.x,z:cmd.z}; pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z); var elevationBonus = cmd.elevationBonus || 0; var range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, extendedName) { var name = extendedName; // Special case for garrisoned units which have a extended template if (extendedName.indexOf("&") != -1) name = extendedName.slice(extendedName.indexOf("&")+1); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(name); if (!template) return null; return GetTemplateDataHelper(template, player); }; GuiInterface.prototype.GetTechnologyData = function(player, name) { var cmpTechnologyTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager); var template = cmpTechnologyTemplateManager.GetTemplate(name); if (!template) { warn("Tried to get data for invalid technology: " + name); return null; } var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); return GetTechnologyDataHelper(template, cmpPlayer.GetCiv()); }; GuiInterface.prototype.IsTechnologyResearched = function(player, tech) { if (!tech) return true; var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(tech); }; // Checks whether the requirements for this technology have been met GuiInterface.prototype.CheckTechnologyRequirements = function(player, tech) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(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) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; var ret = {}; for (var tech in cmpTechnologyManager.GetTechsStarted()) { ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) }; var cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue); if (cmpProductionQueue) ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress; else ret[tech].progress = 0; } return ret; }; // Returns the battle state of the player. GuiInterface.prototype.GetBattleState = function(player) { var cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (cmpBattleDetection) return cmpBattleDetection.GetState(); else return false; }; // Returns a list of ongoing attacks against the player. GuiInterface.prototype.GetIncomingAttacks = function(player) { var cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); return cmpAttackDetection.GetIncomingAttacks(); }; // Used to show a red square over GUI elements you can't yet afford. GuiInterface.prototype.GetNeededResources = function(player, amounts) { return QueryPlayerIDInterface(player).GetNeededResources(amounts); }; /** * 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) { var 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 == undefined) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var numPlayers = cmpPlayerManager.GetNumPlayers(); notification.players = [-1]; for (var i = 1; i < numPlayers; ++i) notification.players.push(i); } this.timeNotifications.push(notification); this.timeNotifications.sort(function (n1, n2){return 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(playerID) { var 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(playerID) != -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() { var n = this.notifications; this.notifications = []; return n; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { return QueryPlayerIDInterface(wantedPlayer).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) { var r = {}; var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return r; r.name = template.Formation.FormationName; r.tooltip = template.Formation.DisabledTooltip || ""; r.icon = template.Formation.Icon; return r; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { // GetLastFormationName is named in a strange way as it (also) is // the value of the current formation (see Formation.js LoadFormation) if (cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate) return true; } } return false; }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmpUnitAI.GetStanceName() == data.stance) return true; } } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { var buildableEnts = []; for each (var ent in cmd.entities) { var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (var building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerColors = {}; // cache of owner -> color map for each (var ent in cmd.entities) { var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color: var owner = -1; var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); var color = playerColors[owner]; if (!color) { color = {"r":1, "g":1, "b":1}; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player); if (cmpPlayer) color = cmpPlayer.GetColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({"r":color.r, "g":color.g, "b":color.b, "a":cmd.alpha}, cmd.selected); } }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return [...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); 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.GetPlayerEntities = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player); }; /** * 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) { var cmpPlayer = QueryPlayerIDInterface(player); // If there are some rally points already displayed, first hide them for each (var ent in this.entsRallyPointsDisplayed) { var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities for each (var ent in cmd.entities) { var 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) var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner var 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 var pos; if (cmd.x && cmd.z) pos = cmd; else pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set if (pos) { // Only update the position if we changed it (cmd.queued is set) if ("queued" in cmd) if (cmd.queued == true) cmpRallyPointRenderer.AddPosition({'x': pos.x, 'y': pos.z}); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z else cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z // rebuild the renderer when not set (when reading saved game or in case of building update) else if (!cmpRallyPointRenderer.IsSet()) for each (var posi in cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition({'x': posi.x, 'y': posi.z}); cmpRallyPointRenderer.SetDisplayed(true); // remember which entities have their rally points displayed so we can hide them again this.entsRallyPointsDisplayed.push(ent); } } }; /** * 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) { var result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [], }; // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) { // Destroy the old preview if there was one if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); // Load the new template if (cmd.template == "") { this.placementEntity = undefined; } else { this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } } if (this.placementEntity) { var ent = this.placementEntity[1]; // Move the preview into the right location var pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } 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) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); // Set it to a red shade if this is an invalid location var 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': ..., * 'populationBonus': ..., * } * } * * @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) { var wallSet = cmd.wallSet; var start = { "pos": cmd.start, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; var end = { "pos": cmd.end, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; // -------------------------------------------------------------------------------- // 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 var numCleared = 0; for (var tpl in this.placementWallEntities) { for each (var ent in this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // keep template data around } return false; } else { // Move all existing cached entities outside of the world and reset their use count for (var tpl in this.placementWallEntities) { for each (var ent in this.placementWallEntities[tpl].entities) { var 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 each (var tpl in wallSet.templates) { if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, tpl), }; // ensure that the loaded template data contains a wallPiece component 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) { var snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error var 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) { var 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 var result = { "pieces": [], "cost": {"food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0}, }; var previewEntities = []; if (end.pos) previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js // 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) { var startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length > 0 && 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 var startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { var 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 > 0 ? previewEntities[0].angle : this.placementWallLastAngle) }); } if (end.pos) { // Analogous to the starting side case above if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { var 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 var endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { var 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 > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle) }); } } var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } var 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. var allPiecesValid = true; var numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity for (var i = 0; i < previewEntities.length; ++i) { var entInfo = previewEntities[i]; var ent = null; var tpl = entInfo.template; var tplData = this.placementWallEntities[tpl].templateData; var entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { // allocate new entity ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else { // reuse an existing one 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 var 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) { var terrainGroundPrev = null; var 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) { var targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } var 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. var primaryControlGroup = ent; var 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); // check whether this wall piece can be validly positioned here var validPlacement = false; var 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 var visible = (cmpRangeManager.GetLosVisibility(ent, player) != "hidden"); if (visible) { var 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. result.cost.food += tplData.cost.food; result.cost.wood += tplData.cost.wood; result.cost.stone += tplData.cost.stone; result.cost.metal += tplData.cost.metal; result.cost.population += tplData.cost.population; result.cost.populationBonus += tplData.cost.populationBonus; result.cost.time += tplData.cost.time; } var canAfford = true; var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) var canAfford = false; var 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) { var 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) var minDist2 = -1; var minDistEntitySnapData = null; var radius2 = data.snapRadius * data.snapRadius; for each (var ent in data.snapEntities) { var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; var pos = cmpPosition.GetPosition(); var 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 (template.BuildRestrictions.Category == "Dock") { var angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return {"x": data.x, "z": data.z, "angle": angle}; } return false; }; GuiInterface.prototype.PlaySound = function(player, data) { // Ignore if no entity was passed if (!data.entity) return; PlaySound(data.name, data.entity); }; GuiInterface.prototype.FindIdleUnits = function(player, data) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerEntities = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) { var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned()) return false; var cmpIdentity = Engine.QueryInterface(e, IID_Identity); if (!cmpIdentity || !cmpIdentity.HasClass(data.idleClass)) return false; return true; }); var idleUnits = []; for (let ent of playerEntities) { if (ent <= data.prevUnit|0 || data.excludeUnits.indexOf(ent) > -1) continue; idleUnits.push(ent); if (data.limit && idleUnits.length >= data.limit) break; } return idleUnits; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { var cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; var firstMarket = cmpEntityTrader.GetFirstMarket(); var secondMarket = cmpEntityTrader.GetSecondMarket(); var result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGain(); } else if (data.target === secondMarket) { result = { "type": "is second", "gain": cmpEntityTrader.GetGain(), }; } else if (!firstMarket) { result = {"type": "set first"}; } else if (!secondMarket) { result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; } else { // Else both markets are not null and target is different from them result = {"type": "set first"}; } return result; }; GuiInterface.prototype.CanCapture = function(player, data) { var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); if (!cmpAttack) return false; var owner = QueryOwnerInterface(data.entity).GetPlayerID(); var cmpCapturable = QueryMiragedInterface(data.target, IID_Capturable); if (cmpCapturable && cmpCapturable.CanCapture(owner) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1) return cmpAttack.CanAttack(data.target); return false; }; GuiInterface.prototype.CanAttack = function(player, data) { var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); if (!cmpAttack) return false; var cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player); var cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player); if (!cmpEntityPlayer || !cmpTargetPlayer) return false; // if the owner is an enemy, it's up to the attack component to decide if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID())) return cmpAttack.CanAttack(data.target); return false; }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { var cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue); if (!cmpProductionQueue) return 0; return cmpProductionQueue.GetBatchTime(data.batchSize); }; GuiInterface.prototype.IsMapRevealed = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); cmpPathfinder.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled) { var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); cmpPathfinder.SetHierDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); cmpObstructionManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for each (var ent in data.entities) { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var traders = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) { return Engine.QueryInterface(e, IID_Trader); }); var landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; var shipTrader = { "total": 0, "trading": 0 }; for each (var ent in traders) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); var 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") { var holder = cmpUnitAI.order.data.target; var 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) { return QueryPlayerIDInterface(player).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.) var exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetExtendedEntityState": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "GetTechnologyData": 1, "IsTechnologyResearched": 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, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "DisplayRallyPoint": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanCapture": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetPathfinderHierDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); else throw new Error("Invalid GuiInterface Call name \""+name+"\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 17407) +++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 17408) @@ -1,341 +1,349 @@ function Health() {} Health.prototype.Schema = "Deals with hitpoints and death." + "" + "100" + "1.0" + "corpse" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "vanish" + "corpse" + "remain" + "" + "" + + "" + + "" + + "" + "" + "" + ""; Health.prototype.Init = function() { // Cache this value so it allows techs to maintain previous health level this.maxHitpoints = +this.template.Max; // Default to , but use if it's undefined or zero // (Allowing 0 initial HP would break our death detection code) this.hitpoints = +(this.template.Initial || this.GetMaxHitpoints()); this.regenRate = ApplyValueModificationsToEntity("Health/RegenRate", +this.template.RegenRate, this.entity); this.CheckRegenTimer(); }; //// Interface functions //// /** * Returns the current hitpoint value. * This is 0 if (and only if) the unit is dead. */ Health.prototype.GetHitpoints = function() { return this.hitpoints; }; Health.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; }; Health.prototype.SetHitpoints = function(value) { // If we're already dead, don't allow resurrection if (this.hitpoints == 0) return; // Before changing the value, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); var old = this.hitpoints; this.hitpoints = Math.max(1, Math.min(this.GetMaxHitpoints(), value)); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) { if (this.hitpoints < this.GetMaxHitpoints()) cmpRangeManager.SetEntityFlag(this.entity, "injured", true); else cmpRangeManager.SetEntityFlag(this.entity, "injured", false); } Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints }); }; Health.prototype.IsRepairable = function() { return Engine.QueryInterface(this.entity, IID_Repairable) != null; }; Health.prototype.IsUnhealable = function() { return (this.template.Unhealable == "true" || this.GetHitpoints() <= 0 || this.GetHitpoints() >= this.GetMaxHitpoints()); }; +Health.prototype.IsUndeletable = function() +{ + return this.template.Undeletable == "true"; +}; + Health.prototype.GetRegenRate = function() { return this.regenRate; }; Health.prototype.ExecuteRegeneration = function() { var regen = this.GetRegenRate(); if (regen > 0) this.Increase(regen); else this.Reduce(-regen); }; /* * Check if the regeneration timer needs to be started or stopped */ Health.prototype.CheckRegenTimer = function() { // check if we need a timer if (this.GetRegenRate() == 0 || this.GetHitpoints() == this.GetMaxHitpoints() && this.GetRegenRate() > 0 || this.GetHitpoints() == 0) { // we don't need a timer, disable if one exists if (this.regenTimer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.regenTimer); this.regenTimer = undefined; } return; } // we need a timer, enable if one doesn't exist if (this.regenTimer) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.regenTimer = cmpTimer.SetInterval(this.entity, IID_Health, "ExecuteRegeneration", 1000, 1000, null); }; Health.prototype.Kill = function() { this.Reduce(this.hitpoints); }; /** * Reduces entity's health by amount HP. * Returns object of the form { "killed": false, "change": -12 } */ Health.prototype.Reduce = function(amount) { // Before changing the value, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); var state = { "killed": false }; if (amount >= 0 && this.hitpoints == this.GetMaxHitpoints()) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) cmpRangeManager.SetEntityFlag(this.entity, "injured", true); } var oldHitpoints = this.hitpoints; if (amount >= this.hitpoints) { // If this is the first time we reached 0, then die. // (The entity will exist a little while after calling DestroyEntity so this // might get called multiple times) if (this.hitpoints) { state.killed = true; PlaySound("death", this.entity); // If SpawnEntityOnDeath is set, spawn the entity if(this.template.SpawnEntityOnDeath) this.CreateDeathSpawnedEntity(); if (this.template.DeathType == "corpse") { this.CreateCorpse(); Engine.DestroyEntity(this.entity); } else if (this.template.DeathType == "vanish") { Engine.DestroyEntity(this.entity); } else if (this.template.DeathType == "remain") { var resource = this.CreateCorpse(true); if (resource != INVALID_ENTITY) Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: resource }); Engine.DestroyEntity(this.entity); } this.hitpoints = 0; Engine.PostMessage(this.entity, MT_HealthChanged, { "from": oldHitpoints, "to": this.hitpoints }); } } else { this.hitpoints -= amount; Engine.PostMessage(this.entity, MT_HealthChanged, { "from": oldHitpoints, "to": this.hitpoints }); } state.change = this.hitpoints - oldHitpoints; return state; }; Health.prototype.Increase = function(amount) { // Before changing the value, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); if (this.hitpoints == this.GetMaxHitpoints()) return {"old": this.hitpoints, "new":this.hitpoints}; // If we're already dead, don't allow resurrection if (this.hitpoints == 0) return undefined; var old = this.hitpoints; this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints()); if (this.hitpoints == this.GetMaxHitpoints()) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) cmpRangeManager.SetEntityFlag(this.entity, "injured", false); } Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints }); // We return the old and the actual hp return { "old": old, "new": this.hitpoints}; }; //// Private functions //// Health.prototype.CreateCorpse = function(leaveResources) { // If the unit died while not in the world, don't create any corpse for it // since there's nowhere for the corpse to be placed var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition.IsInWorld()) return INVALID_ENTITY; // Either creates a static local version of the current entity, or a // persistent corpse retaining the ResourceSupply element of the parent. var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); var corpse; if (leaveResources) corpse = Engine.AddEntity("resource|" + templateName); else corpse = Engine.AddLocalEntity("corpse|" + templateName); // Copy various parameters so it looks just like us var cmpCorpsePosition = Engine.QueryInterface(corpse, IID_Position); var pos = cmpPosition.GetPosition(); cmpCorpsePosition.JumpTo(pos.x, pos.z); var rot = cmpPosition.GetRotation(); cmpCorpsePosition.SetYRotation(rot.y); cmpCorpsePosition.SetXZRotation(rot.x, rot.z); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpCorpseOwnership = Engine.QueryInterface(corpse, IID_Ownership); cmpCorpseOwnership.SetOwner(cmpOwnership.GetOwner()); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); var cmpCorpseVisual = Engine.QueryInterface(corpse, IID_Visual); cmpCorpseVisual.SetActorSeed(cmpVisual.GetActorSeed()); // Make it fall over cmpCorpseVisual.SelectAnimation("death", true, 1.0, ""); return corpse; }; Health.prototype.CreateDeathSpawnedEntity = function() { // If the unit died while not in the world, don't spawn a death entity for it // since there's nowhere for it to be placed var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition.IsInWorld()) return INVALID_ENTITY; // Create SpawnEntityOnDeath entity var spawnedEntity = Engine.AddLocalEntity(this.template.SpawnEntityOnDeath); // Move to same position var cmpSpawnedPosition = Engine.QueryInterface(spawnedEntity, IID_Position); var pos = cmpPosition.GetPosition(); cmpSpawnedPosition.JumpTo(pos.x, pos.z); var rot = cmpPosition.GetRotation(); cmpSpawnedPosition.SetYRotation(rot.y); cmpSpawnedPosition.SetXZRotation(rot.x, rot.z); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpSpawnedOwnership = Engine.QueryInterface(spawnedEntity, IID_Ownership); if (cmpOwnership && cmpSpawnedOwnership) cmpSpawnedOwnership.SetOwner(cmpOwnership.GetOwner()); return spawnedEntity; }; Health.prototype.OnValueModification = function(msg) { if (msg.component != "Health") return; var oldMaxHitpoints = this.GetMaxHitpoints(); var newMaxHitpoints = Math.round(ApplyValueModificationsToEntity("Health/Max", +this.template.Max, this.entity)); if (oldMaxHitpoints != newMaxHitpoints) { var newHitpoints = Math.round(this.GetHitpoints() * newMaxHitpoints/oldMaxHitpoints); this.maxHitpoints = newMaxHitpoints; this.SetHitpoints(newHitpoints); } var oldRegenRate = this.regenRate; this.regenRate = ApplyValueModificationsToEntity("Health/RegenRate", +this.template.RegenRate, this.entity); if (this.regenRate != oldRegenRate) this.CheckRegenTimer(); }; Health.prototype.OnHealthChanged = function() { this.CheckRegenTimer(); }; Engine.RegisterComponentType(IID_Health, "Health", Health); Index: ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 17407) +++ ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 17408) @@ -1,138 +1,141 @@ const VIS_HIDDEN = 0; const VIS_FOGGED = 1; const VIS_VISIBLE = 2; function Mirage() {} Mirage.prototype.Schema = "Mirage entities replace real entities in the fog-of-war." + ""; Mirage.prototype.Init = function() { this.player = null; this.parent = INVALID_ENTITY; this.miragedIids = new Set(); this.buildPercentage = 0; this.numBuilders = 0; this.maxHitpoints = null; this.hitpoints = null; this.repairable = null; this.unhealable = null; + this.undeletable = null; this.capturePoints = []; this.maxCapturePoints = 0; this.maxAmount = null; this.amount = null; this.type = null; this.isInfinite = null; this.killBeforeGather = null; this.maxGatherers = null; this.numGatherers = null; }; Mirage.prototype.SetParent = function(ent) { this.parent = ent; }; Mirage.prototype.GetPlayer = function() { return this.player; }; Mirage.prototype.SetPlayer = function(player) { this.player = player; }; Mirage.prototype.Mirages = function(iid) { return this.miragedIids.has(iid); }; // ============================ // Parent entity data // Foundation data Mirage.prototype.CopyFoundation = function(cmpFoundation) { this.miragedIids.add(IID_Foundation); this.buildPercentage = cmpFoundation.GetBuildPercentage(); this.numBuilders = cmpFoundation.GetNumBuilders(); }; Mirage.prototype.GetBuildPercentage = function() { return this.buildPercentage; }; Mirage.prototype.GetNumBuilders = function() { return this.numBuilders; }; // Health data Mirage.prototype.CopyHealth = function(cmpHealth) { this.miragedIids.add(IID_Health); this.maxHitpoints = cmpHealth.GetMaxHitpoints(); this.hitpoints = cmpHealth.GetHitpoints(); this.repairable = cmpHealth.IsRepairable(); this.unhealable = cmpHealth.IsUnhealable(); + this.undeletable = cmpHealth.IsUndeletable(); }; Mirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; }; Mirage.prototype.GetHitpoints = function() { return this.hitpoints; }; Mirage.prototype.IsRepairable = function() { return this.repairable; }; Mirage.prototype.IsUnhealable = function() { return this.unhealable; }; +Mirage.prototype.IsUndeletable = function() { return this.undeletable; }; // Capture data Mirage.prototype.CopyCapturable = function(cmpCapturable) { this.miragedIids.add(IID_Capturable); this.capturePoints = clone(cmpCapturable.GetCapturePoints()); this.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); }; Mirage.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; }; Mirage.prototype.GetCapturePoints = function() { return this.capturePoints; }; Mirage.prototype.CanCapture = Capturable.prototype.CanCapture; // ResourceSupply data Mirage.prototype.CopyResourceSupply = function(cmpResourceSupply) { this.miragedIids.add(IID_ResourceSupply); this.maxAmount = cmpResourceSupply.GetMaxAmount(); this.amount = cmpResourceSupply.GetCurrentAmount(); this.type = cmpResourceSupply.GetType(); this.isInfinite = cmpResourceSupply.IsInfinite(); this.killBeforeGather = cmpResourceSupply.GetKillBeforeGather(); this.maxGatherers = cmpResourceSupply.GetMaxGatherers(); this.numGatherers = cmpResourceSupply.GetNumGatherers(); }; Mirage.prototype.GetMaxAmount = function() { return this.maxAmount; }; Mirage.prototype.GetCurrentAmount = function() { return this.amount; }; Mirage.prototype.GetType = function() { return this.type; }; Mirage.prototype.IsInfinite = function() { return this.isInfinite; }; Mirage.prototype.GetKillBeforeGather = function() { return this.killBeforeGather; }; Mirage.prototype.GetMaxGatherers = function() { return this.maxGatherers; }; Mirage.prototype.GetNumGatherers = function() { return this.numGatherers; }; // ============================ Mirage.prototype.OnVisibilityChanged = function(msg) { if (msg.player != this.player || msg.newVisibility != VIS_HIDDEN) return; if (this.parent == INVALID_ENTITY) Engine.DestroyEntity(this.entity); else Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: this.parent }); }; Engine.RegisterComponentType(IID_Mirage, "Mirage", Mirage); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 17407) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 17408) @@ -1,1642 +1,1644 @@ // Setting this to true will display some warnings when commands // are likely to fail, which may be useful for debugging AIs var g_DebugCommands = false; function ProcessCommand(player, cmd) { // Do some basic checks here that commanding player is valid var data = {}; data.cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!data.cmpPlayerMan || player < 0) return; data.playerEnt = data.cmpPlayerMan.GetPlayerByID(player); if (data.playerEnt == INVALID_ENTITY) return; data.cmpPlayer = Engine.QueryInterface(data.playerEnt, IID_Player); if (!data.cmpPlayer) return; data.controlAllUnits = data.cmpPlayer.CanControlAllUnits(); if (cmd.entities) data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits); // Note: checks of UnitAI targets are not robust enough here, as ownership // can change after the order is issued, they should be checked by UnitAI // when the specific behavior (e.g. attack, garrison) is performed. // (Also it's not ideal if a command silently fails, it's nicer if UnitAI // moves the entities closer to the target before giving up.) // Now handle various commands if (g_Commands[cmd.type]) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("PlayerCommand", {"player": player, "cmd": cmd}); g_Commands[cmd.type](player, cmd, data); } else error("Invalid command: unknown command type: "+uneval(cmd)); } var g_Commands = { "debug-print": function(player, cmd, data) { print(cmd.message); }, "chat": function(player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": cmd.type, "players": [player], "message": cmd.message}); }, "aichat": function(player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); var notification = { "players": [player] }; for (var key in cmd) notification[key] = cmd[key]; cmpGuiInterface.PushNotification(notification); }, "cheat": function(player, cmd, data) { Cheat(cmd); }, "quit": function(player, cmd, data) { // Let the AI exit the game for testing purposes var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "quit", "players": [player]}); }, "diplomacy": function(player, cmd, data) { switch(cmd.to) { case "ally": data.cmpPlayer.SetAlly(cmd.player); break; case "neutral": data.cmpPlayer.SetNeutral(cmd.player); break; case "enemy": data.cmpPlayer.SetEnemy(cmd.player); break; default: warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "diplomacy", "players": [player], "player1": cmd.player, "status": cmd.to}); }, "tribute": function(player, cmd, data) { data.cmpPlayer.TributeResource(cmd.player, cmd.amounts); }, "control-all": function(player, cmd, data) { data.cmpPlayer.SetControlAllUnits(cmd.flag); }, "reveal-map": function(player, cmd, data) { // Reveal the map for all players, not just the current player, // primarily to make it obvious to everyone that the player is cheating var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, cmd.enable); }, "walk": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); }); }, "walk-to-range": function(player, cmd, data) { // Only used by the AI for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(cmpUnitAI) cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued); } }, "attack-walk": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued); }); }, "attack": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) { // This check is for debugging only! warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); } if (cmd.allowCapture == null) cmd.allowCapture = true; // See UnitAI.CanAttack for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Attack(cmd.target, cmd.queued, cmd.allowCapture); }); }, "heal": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) { // This check is for debugging only! warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); } // See UnitAI.CanHeal for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Heal(cmd.target, cmd.queued); }); }, "repair": function(player, cmd, data) { // This covers both repairing damaged buildings, and constructing unfinished foundations if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) { // This check is for debugging only! warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); } // See UnitAI.CanRepair for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); }); }, "gather": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) { // This check is for debugging only! warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); } // See UnitAI.CanGather for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Gather(cmd.target, cmd.queued); }); }, "gather-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued); }); }, "returnresource": function(player, cmd, data) { // Check dropsite is owned by player if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) { // This check is for debugging only! warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); } // See UnitAI.CanReturnResource for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.ReturnResource(cmd.target, cmd.queued); }); }, "back-to-work": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(!cmpUnitAI || !cmpUnitAI.BackToWork()) notifyBackToWorkFailure(player); } }, "remove-guard": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(cmpUnitAI) cmpUnitAI.RemoveGuard(); } }, "train": function(player, cmd, data) { // Check entity limits var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.GetTemplate(cmd.template); var unitCategory = null; if (template.TrainingRestrictions) unitCategory = template.TrainingRestrictions.Category; // Verify that the building(s) can be controlled by the player if (data.entities.length <= 0) { if (g_DebugCommands) warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd)); return; } for each (var ent in data.entities) { if (unitCategory) { var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits); if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count)) { if (g_DebugCommands) warn(unitCategory + " train limit is reached: " + uneval(cmd)); continue; } } var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); if (!cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: training requires unresearched technology: " + uneval(cmd)); continue; } var queue = Engine.QueryInterface(ent, IID_ProductionQueue); // Check if the building can train the unit // TODO: the AI API does not take promotion technologies into account for the list // of trainable units (taken directly from the unit template). Here is a temporary fix. if (queue && data.cmpPlayer.IsAI()) { var list = queue.GetEntitiesList(); if (list.indexOf(cmd.template) === -1 && cmd.promoted) { for (var promoted of cmd.promoted) { if (list.indexOf(promoted) === -1) continue; cmd.template = promoted; break; } } } if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1) if ("metadata" in cmd) queue.AddBatch(cmd.template, "unit", +cmd.count, cmd.metadata); else queue.AddBatch(cmd.template, "unit", +cmd.count); } }, "research": function(player, cmd, data) { // Verify that the building can be controlled by the player if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: research building cannot be controlled by player "+player+": "+uneval(cmd)); return; } var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager); if (!cmpTechnologyManager.CanResearch(cmd.template)) { if (g_DebugCommands) warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd)); return; } var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (queue) queue.AddBatch(cmd.template, "technology"); }, "stop-production": function(player, cmd, data) { // Verify that the building can be controlled by the player if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: production building cannot be controlled by player "+player+": "+uneval(cmd)); return; } var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (queue) queue.RemoveBatch(cmd.id); }, "construct": function(player, cmd, data) { TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "construct-wall": function(player, cmd, data) { TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "delete-entities": function(player, cmd, data) { for (let ent of data.entities) { // don't allow to delete entities who are half-captured var cmpCapturable = Engine.QueryInterface(ent, IID_Capturable); if (cmpCapturable) { var capturePoints = cmpCapturable.GetCapturePoints(); var maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); if (capturePoints[player] < maxCapturePoints / 2) return; } // either kill or delete the entity var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) { + if (cmpHealth.IsUndeletable()) + continue; var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (!cmpResourceSupply || !cmpResourceSupply.GetKillBeforeGather()) cmpHealth.Kill(); } else Engine.DestroyEntity(ent); } }, "set-rallypoint": function(player, cmd, data) { for each (var ent in data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { if (!cmd.queued) cmpRallyPoint.Unset(); cmpRallyPoint.AddPosition(cmd.x, cmd.z); cmpRallyPoint.AddData(cmd.data); } } }, "unset-rallypoint": function(player, cmd, data) { for each (var ent in data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Reset(); } }, "defeat-player": function(player, cmd, data) { // Send "OnPlayerDefeated" message to player Engine.PostMessage(data.playerEnt, MT_PlayerDefeated, { "playerId": player } ); }, "garrison": function(player, cmd, data) { // Verify that the building can be controlled by the player or is mutualAlly if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Garrison(cmd.target, cmd.queued); }); }, "guard": function(player, cmd, data) { // Verify that the target can be controlled by the player or is mutualAlly if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Guard(cmd.target, cmd.queued); }); }, "stop": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Stop(cmd.queued); }); }, "unload": function(player, cmd, data) { // Verify that the building can be controlled by the player or is mutualAlly if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); var notUngarrisoned = 0; // The owner can ungarrison every garrisoned unit if (IsOwnedByPlayer(player, cmd.garrisonHolder)) data.entities = cmd.entities; for each (var ent in data.entities) if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent)) notUngarrisoned++; if (notUngarrisoned != 0) notifyUnloadFailure(player, cmd.garrisonHolder); }, "unload-template": function(player, cmd, data) { var index = cmd.template.indexOf("&"); // Templates for garrisoned units are extended if (index == -1) return; var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for each (var garrisonHolder in entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Only the owner of the garrisonHolder may unload entities from any owners if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits && player != +cmd.template.slice(1,index)) continue; if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.all)) notifyUnloadFailure(player, garrisonHolder); } } }, "unload-all-own": function(player, cmd, data) { var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits); for each (var garrisonHolder in entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllOwn()) notifyUnloadFailure(player, garrisonHolder); } }, "unload-all-by-owner": function(player, cmd, data) { var entities = cmd.garrisonHolders; for (var 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 each (var garrisonHolder in entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll()) notifyUnloadFailure(player, garrisonHolder); } }, "increase-alert-level": function(player, cmd, data) { for each (var ent in data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (!cmpAlertRaiser || !cmpAlertRaiser.IncreaseAlertLevel()) notifyAlertFailure(player); } }, "alert-end": function(player, cmd, data) { for each (var ent in data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.EndOfAlert(); } }, "formation": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd.name).forEach(function(cmpUnitAI) { cmpUnitAI.MoveIntoFormation(cmd); }); }, "promote": function(player, cmd, data) { // No need to do checks here since this is a cheat anyway var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "chat", "players": [player], "message": "(Cheat - promoted units)"}); for each (var ent in cmd.entities) { var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp()); } }, "stance": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && !cmpUnitAI.IsTurret()) cmpUnitAI.SwitchToStance(cmd.name); } }, "wall-to-gate": function(player, cmd, data) { for each (var ent in data.entities) { TryTransformWallToGate(ent, data.cmpPlayer, cmd.template); } }, "lock-gate": function(player, cmd, data) { for each (var ent in data.entities) { var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) { if (cmd.lock) cmpGate.LockGate(); else cmpGate.UnlockGate(); } } }, "setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); }); }, "select-required-goods": function(player, cmd, data) { for each (var ent in data.entities) { var cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) cmpTrader.SetRequiredGoods(cmd.requiredGoods); } }, "set-trading-goods": function(player, cmd, data) { data.cmpPlayer.SetTradingGoods(cmd.tradingGoods); }, "barter": function(player, cmd, data) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); cmpBarter.ExchangeResources(data.playerEnt, cmd.sell, cmd.buy, cmd.amount); }, "set-shading-color": function(player, cmd, data) { // Debug command to make an entity brightly colored for each (var ent in cmd.entities) { var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0 } }, "pack": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmd.pack) cmpUnitAI.Pack(cmd.queued); else cmpUnitAI.Unpack(cmd.queued); } } }, "cancel-pack": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmd.pack) cmpUnitAI.CancelPack(cmd.queued); else cmpUnitAI.CancelUnpack(cmd.queued); } } }, "attack-request": function(player, cmd, data) { // Send a chat message to human players var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGuiInterface) { var notification = { "type": "aichat", "players": [player], "message": "/allies " + markForTranslation("Attack against %(_player_)s requested."), "translateParameters": ["_player_"], "parameters": {"_player_": cmd.target} }; cmpGuiInterface.PushNotification(notification); } // And send an attackRequest event to the AIs let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("AttackRequest", cmd); }, "dialog-answer": function(player, cmd, data) { // Currently nothing. Triggers can read it anyway, and send this // message to any component you like. }, }; /** * Sends a GUI notification about unit(s) that failed to ungarrison. */ function notifyUnloadFailure(player, garrisonHolder) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Unable to ungarrison unit(s)" }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } /** * Sends a GUI notification about worker(s) that failed to go back to work. */ function notifyBackToWorkFailure(player) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Some unit(s) can't go back to work" }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } /** * Sends a GUI notification about Alerts that failed to be raised */ function notifyAlertFailure(player) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "You can't raise the alert to a higher level !" }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } /** * Get some information about the formations used by entities. * The entities must have a UnitAI component. */ function ExtractFormations(ents) { var entities = []; // subset of ents that have UnitAI var members = {}; // { formationentity: [ent, ent, ...], ... } for each (var ent in ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var fid = cmpUnitAI.GetFormationController(); if (fid != INVALID_ENTITY) { if (!members[fid]) members[fid] = []; members[fid].push(ent); } entities.push(ent); } var ids = [ id for (id in members) ]; return { "entities": entities, "members": members, "ids": ids }; } /** * Tries to find the best angle to put a dock at a given position * Taken from GuiInterface.js */ function GetDockAngle(template, x, z) { var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) return undefined; // Get footprint size var halfSize = 0; if (template.Footprint.Square) halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; else if (template.Footprint.Circle) halfSize = template.Footprint.Circle["@radius"]; /* Find direction of most open water, algorithm: * 1. Pick points in a circle around dock * 2. If point is in water, add to array * 3. Scan array looking for consecutive points * 4. Find longest sequence of consecutive points * 5. If sequence equals all points, no direction can be determined, * expand search outward and try (1) again * 6. Calculate angle using average of sequence */ const numPoints = 16; for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { var angle = (i/numPoints)*2*Math.PI; var d = halfSize*(dist+1); var nx = x - d*Math.sin(angle); var nz = z + d*Math.cos(angle); if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) waterPoints.push(i); } var consec = []; var length = waterPoints.length; if (!length) continue; for (var i = 0; i < length; ++i) { var count = 0; for (var j = 0; j < (length-1); ++j) { if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } var start = 0; var count = 0; for (var c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI); } return undefined; } /** * Attempts to construct a building using the specified parameters. * Returns true on success, false on failure. */ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd) { // Message structure: // { // "type": "construct", // "entities": [...], // entities that will be ordered to construct the building (if applicable) // "template": "...", // template name of the entity being constructed // "x": ..., // "z": ..., // "angle": ..., // "metadata": "...", // AI metadata of the building // "actorSeed": ..., // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable) // "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. If specified, must be a valid control group ID (> 0). // "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. May be INVALID_ENTITY. // } /* * Construction process: * . Take resources away immediately. * . Create a foundation entity with 1hp, 0% build progress. * . Increase hp and build progress up to 100% when people work on it. * . If it's destroyed, an appropriate fraction of the resource cost is refunded. * . If it's completed, it gets replaced with the real building. */ // Check whether we can control these units var entities = FilterEntityList(cmd.entities, player, controlAllUnits); if (!entities.length) return false; var foundationTemplate = "foundation|" + cmd.template; // Tentatively create the foundation (we might find later that it's a invalid build command) var ent = Engine.AddEntity(foundationTemplate); if (ent == INVALID_ENTITY) { // Error (e.g. invalid template names) error("Error creating foundation entity for '" + cmd.template + "'"); return false; } // If it's a dock, get the right angle. var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateMgr.GetTemplate(cmd.template); if (template.BuildRestrictions.Category === "Dock") { var angle = GetDockAngle(template, cmd.x, cmd.z); if (angle !== undefined) cmd.angle = angle; } // Move the foundation to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(cmd.angle); // Set the obstruction control group if needed if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2) { var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); // primary control group must always be valid if (cmd.obstructionControlGroup) { if (cmd.obstructionControlGroup <= 0) warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0"); cmpObstruction.SetControlGroup(cmd.obstructionControlGroup); } if (cmd.obstructionControlGroup2) cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2); } // Make it owned by the current player var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (cmpBuildRestrictions) { var ret = cmpBuildRestrictions.CheckPlacement(); if (!ret.success) { if (g_DebugCommands) { warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd)); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); ret.players = [player]; cmpGuiInterface.PushNotification(ret); // Remove the foundation because the construction was aborted // move it out of world because it's not destroyed immediately. cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } } else error("cmpBuildRestrictions not defined"); // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (!cmpEntityLimits || !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) { if (g_DebugCommands) { warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd)); } // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) { warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd)); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "players": [player], "message": "Building's technology requirements are not met." }); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); } // We need the cost after tech modifications // To calculate this with an entity requires ownership, so use the template instead var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(foundationTemplate); var costs = {}; for (var r in template.Cost.Resources) { costs[r] = +template.Cost.Resources[r]; if (cmpTechnologyManager) costs[r] = cmpTechnologyManager.ApplyModificationsTemplate("Cost/Resources/"+r, costs[r], template); } if (!cmpPlayer.TrySubtractResources(costs)) { if (g_DebugCommands) warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd)); Engine.DestroyEntity(ent); cmpPosition.MoveOutOfWorld(); return false; } var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual && cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); // Initialise the foundation var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); cmpFoundation.InitialiseConstruction(player, cmd.template); // send Metadata info if any if (cmd.metadata) Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } ); // Tell the units to start building this new entity if (cmd.autorepair) { ProcessCommand(player, { "type": "repair", "entities": entities, "target": ent, "autocontinue": cmd.autocontinue, "queued": cmd.queued }); } return ent; } function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd) { // 'cmd' message structure: // { // "type": "construct-wall", // "entities": [...], // entities that will be ordered to construct the wall (if applicable) // "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...) // { // "template": "...", // one of the templates from the wallset // "x": ..., // "z": ..., // "angle": ..., // }, // ... // ], // "wallSet": { // "templates": { // "tower": // tower template name // "long": // long wall segment template name // ... // etc. // }, // "maxTowerOverlap": ..., // "minTowerOverlap": ..., // }, // "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall // "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable) // } if (cmd.pieces.length <= 0) return; if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side"); return; } if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side"); return; } // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing // towers in the case of snapping). The towers themselves all keep their default unique control groups. // To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour // it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the // first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of // the first tower encountered towards the ending side of the wall (if any). // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the // wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes: // // FIRST PASS: // - Go from start to end and construct wall piece foundations as far as we can without running into a piece that // cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID // as the primary control group, thus allowing it to be built overlapping the previous piece. // - If we encounter a new tower along the way (which will gain its own control group), do the following: // o First build it using temporarily the same control group of the previous (non-tower) piece // o Set the previous piece's secondary control group to the tower's entity ID // o Restore the primary control group of the constructed tower back its original (unique) value. // The temporary control group is necessary to allow the newer tower with its unique control group ID to be able // to be placed while overlapping the previous piece. // // SECOND PASS: // - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this // time registering the right neighbouring tower in each non-tower piece. // first pass; L -> R var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that // the first wall piece can be built while overlapping it. if (cmd.startSnappedEntity) { var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction); if (!cmpSnappedStartObstruction) { error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup(); //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup); } var i = 0; var queued = cmd.queued; for (; i < cmd.pieces.length; ++i) { var piece = cmd.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 == cmd.pieces.length - 1 && cmd.endSnappedEntity) { var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (cmpEndSnappedObstruction) constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup(); } var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd); if (pieceEntityId) { // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later piece.ent = pieceEntityId; // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex if (piece.template == cmd.wallSet.templates.tower) { var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction); var newTowerControlGroup = pieceEntityId; if (i > 0) { //warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup); var cmpPreviousObstruction = Engine.QueryInterface(cmd.pieces[i-1].ent, IID_Obstruction); // TODO: ensure that cmpPreviousObstruction exists // TODO: ensure that the previous obstruction does not yet have a secondary control group set cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup); } // TODO: ensure that cmpTowerObstruction exists cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group lastTowerIndex = i; lastTowerControlGroup = newTowerControlGroup; } } else { // failed to build wall piece, abort i = j + 1; // compensate for the -1 subtracted by lastBuiltPieceIndex below break; } } var lastBuiltPieceIndex = i - 1; var wallComplete = (lastBuiltPieceIndex == cmd.pieces.length - 1); // At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower). // Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any) // as their secondary control groups. lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // only start off with the ending side's snapped tower's control group if we were able to build the entire wall if (cmd.endSnappedEntity && wallComplete) { var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (!cmpSnappedEndObstruction) { error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup(); } for (var j = lastBuiltPieceIndex; j >= 0; --j) { var piece = cmd.pieces[j]; if (!piece.ent) { error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'"); continue; } var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction); if (!cmpPieceObstruction) { error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component"); continue; } if (piece.template == cmd.wallSet.templates.tower) { // encountered a tower entity, update the last tower control group lastTowerControlGroup = cmpPieceObstruction.GetControlGroup(); } else { // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'. // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'. var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2(); if (existingSecondaryControlGroup == INVALID_ENTITY) { if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY) { cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup); } } else if (existingSecondaryControlGroup != lastTowerControlGroup) { error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")"); break; } } } } /** * Remove the given list of entities from their current formations. */ function RemoveFromFormation(ents) { var formation = ExtractFormations(ents); for (var fid in formation.members) { var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } } /** * Returns a list of UnitAI components, each belonging either to a * selected unit or to a formation entity for groups of the selected units. */ function GetFormationUnitAIs(ents, player, formationTemplate) { // If an individual was selected, remove it from any formation // and command it individually if (ents.length == 1) { // Skip unit if it has no UnitAI var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); if (!cmpUnitAI) return []; RemoveFromFormation(ents); return [ cmpUnitAI ]; } // Separate out the units that don't support the chosen formation var formedEnts = []; var nonformedUnitAIs = []; for each (var ent in ents) { // Skip units with no UnitAI or no position var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) continue; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: We only check if the formation is usable by some units // if we move them to it. We should check if we can use formations // for the other cases. var nullFormation = (formationTemplate || cmpUnitAI.GetLastFormationTemplate()) == "formations/null"; if (!nullFormation && cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "formations/null")) formedEnts.push(ent); else { if (nullFormation) cmpUnitAI.SetLastFormationTemplate("formations/null"); nonformedUnitAIs.push(cmpUnitAI); } } if (formedEnts.length == 0) { // No units support the foundation - return all the others return nonformedUnitAIs; } // Find what formations the formationable selected entities are currently in var formation = ExtractFormations(formedEnts); var formationUnitAIs = []; if (formation.ids.length == 1) { // Selected units either belong to this formation or have no formation // Check that all its members are selected var fid = formation.ids[0]; var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length && cmpFormation.GetMemberCount() == formation.entities.length) { cmpFormation.DeleteTwinFormations(); // The whole formation was selected, so reuse its controller for this command formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate)) cmpFormation.LoadFormation(formationTemplate); } } if (!formationUnitAIs.length) { // We need to give the selected units a new formation controller // Remove selected units from their current formation for (var fid in formation.members) { var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } // TODO replace the fixed 60 with something sensible, based on vision range f.e. var formationSeparation = 60; var clusters = ClusterEntities(formation.entities, formationSeparation); var formationEnts = []; for each (var cluster in clusters) { if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) { // get the most recently used formation, or default to line closed var lastFormationTemplate = undefined; for each (var ent in cluster) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { var template = cmpUnitAI.GetLastFormationTemplate(); if (lastFormationTemplate === undefined) { lastFormationTemplate = template; } else if (lastFormationTemplate != template) { lastFormationTemplate = undefined; break; } } } if (lastFormationTemplate && CanMoveEntsIntoFormation(cluster, lastFormationTemplate)) formationTemplate = lastFormationTemplate; else formationTemplate = "formations/null"; } // Create the new controller var formationEnt = Engine.AddEntity(formationTemplate); var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); cmpFormation.SetFormationSeparation(formationSeparation); cmpFormation.SetMembers(cluster); for each (var ent in formationEnts) cmpFormation.RegisterTwinFormation(ent); formationEnts.push(formationEnt); var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); cmpOwnership.SetOwner(player); } } return nonformedUnitAIs.concat(formationUnitAIs); } /** * Group a list of entities in clusters via single-links */ function ClusterEntities(ents, separationDistance) { var clusters = []; if (!ents.length) return clusters; var distSq = separationDistance * separationDistance; var positions = []; // triangular matrix with the (squared) distances between the different clusters // the other half is not initialised var matrix = []; for (var i = 0; i < ents.length; i++) { matrix[i] = []; clusters.push([ents[i]]); var cmpPosition = Engine.QueryInterface(ents[i], IID_Position); positions.push(cmpPosition.GetPosition2D()); for (var j = 0; j < i; j++) matrix[i][j] = positions[i].distanceToSquared(positions[j]); } while (clusters.length > 1) { // search two clusters that are closer than the required distance var smallDist = Infinity; var closeClusters = undefined; for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i) for (var j = i - 1; j >= 0 && !closeClusters; --j) if (matrix[i][j] < distSq) closeClusters = [i,j]; // if no more close clusters found, just return all found clusters so far if (!closeClusters) return clusters; // make a new cluster with the entities from the two found clusters var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]); // calculate the minimum distance between the new cluster and all other remaining // clusters by taking the minimum of the two distances. var distances = []; for (var i = 0; i < clusters.length; i++) { if (i == closeClusters[1] || i == closeClusters[0]) continue; var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]]; var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]]; distances.push(Math.min(dist1, dist2)); } // remove the rows and columns in the matrix for the merged clusters, // and the clusters themselves from the cluster list clusters.splice(closeClusters[0],1); clusters.splice(closeClusters[1],1); matrix.splice(closeClusters[0],1); matrix.splice(closeClusters[1],1); for (var i = 0; i < matrix.length; i++) { if (matrix[i].length > closeClusters[0]) matrix[i].splice(closeClusters[0],1); if (matrix[i].length > closeClusters[1]) matrix[i].splice(closeClusters[1],1); } // add a new row of distances to the matrix and the new cluster clusters.push(newCluster); matrix.push(distances); } return clusters; } function GetFormationRequirements(formationTemplate) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(formationTemplate); if (!template.Formation) return false; return {"minCount": +template.Formation.RequiredMemberCount}; } function CanMoveEntsIntoFormation(ents, formationTemplate) { // TODO: should check the player's civ is allowed to use this formation // See simulation/components/Player.js GetFormations() for a list of all allowed formations var requirements = GetFormationRequirements(formationTemplate); if (!requirements) return false; var count = 0; var reqClasses = requirements.classesRequired || []; for each (var ent in ents) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate)) continue; count++; } return count >= requirements.minCount; } /** * Check if player can control this entity * returns: true if the entity is valid and owned by the player * or control all units is activated, else false */ function CanControlUnit(entity, player, controlAll) { return (IsOwnedByPlayer(player, entity) || controlAll); } /** * Check if player can control this entity * returns: true if the entity is valid and owned by the player * or the entity is owned by an mutualAlly * or control all units is activated, else false */ function CanControlUnitOrIsAlly(entity, player, controlAll) { return (IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll); } /** * Filter entities which the player can control */ function FilterEntityList(entities, player, controlAll) { return entities.filter(function(ent) { return CanControlUnit(ent, player, controlAll);} ); } /** * Filter entities which the player can control or are mutualAlly */ function FilterEntityListWithAllies(entities, player, controlAll) { return entities.filter(function(ent) { return CanControlUnitOrIsAlly(ent, player, controlAll);} ); } /** * Try to transform a wall to a gate */ function TryTransformWallToGate(ent, cmpPlayer, template) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) return; // Check if this is a valid long wall segment if (!cmpIdentity.HasClass("LongWall")) { if (g_DebugCommands) warn("Invalid command: invalid wall conversion to gate for player "+player+": "+uneval(cmd)); return; } var civ = cmpIdentity.GetCiv(); var gate = Engine.AddEntity(template); var cmpCost = Engine.QueryInterface(gate, IID_Cost); if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts())) { if (g_DebugCommands) warn("Invalid command: convert gate cost check failed for player "+player+": "+uneval(cmd)); Engine.DestroyEntity(gate); return; } ReplaceBuildingWith(ent, gate); } /** * Unconditionally replace a building with another one */ function ReplaceBuildingWith(ent, building) { // Move the building to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position); var pos = cmpPosition.GetPosition2D(); cmpBuildingPosition.JumpTo(pos.x, pos.y); var rot = cmpPosition.GetRotation(); cmpBuildingPosition.SetYRotation(rot.y); cmpBuildingPosition.SetXZRotation(rot.x, rot.z); // Copy ownership var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership); cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner()); // Copy control groups var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction); cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup()); cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2()); // Copy health level from the old entity to the new var cmpHealth = Engine.QueryInterface(ent, IID_Health); var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health); var healthFraction = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints())); var buildingHitpoints = Math.round(cmpBuildingHealth.GetMaxHitpoints() * healthFraction); cmpBuildingHealth.SetHitpoints(buildingHitpoints); PlaySound("constructed", building); Engine.PostMessage(ent, MT_ConstructionFinished, { "entity": ent, "newentity": building }); Engine.BroadcastMessage(MT_EntityRenamed, { entity: ent, newentity: building }); Engine.DestroyEntity(ent); } Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements); Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("GetDockAngle", GetDockAngle); Engine.RegisterGlobal("ProcessCommand", ProcessCommand); Engine.RegisterGlobal("g_Commands", g_Commands); Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 17407) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 17408) @@ -1,126 +1,127 @@ 1 1 1 1 1 1 0 0 Ranged Infantry land own 500 0.5 4.0 0 0 10 0 0 0 0 false false 0.0 3.0 9.8 corpse 0 + false true 2.0 Structure Structure ConquestCritical structure true true true true true false false special/rallypoint art/textures/misc/rallypoint_line.png art/textures/misc/rallypoint_line_mask.png 0.2 square round default outline_border.png outline_border_mask.png 0.4 interface/complete/building/complete_universal.xml attack/destruction/building_collapse_large.xml interface/alarm/alarm_attackplayer.xml attack/weapon/arrowfly.xml attack/impact/arrow_metal.xml 6.0 0.6 12.0 20 true false false false 40 false true false Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 17407) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 17408) @@ -1,127 +1,127 @@ 15 25 3 5 15 3 0.0 15.0 0.0 72.0 10.0 75.0 1200 2000 1.5 3 1 own neutral CivilCentre CivilCentre 200 2000 5.0 20 500 0 500 500 500 8.0 20 0.1 Unit Support Infantry Cavalry 1 1 3000 rubble/rubble_stone_6x6 - + Civic Center Build to acquire large tracts of territory. Train citizens. Garrison: 20. Defensive CivCentre CivilCentre structures/civic_centre.png 200 0 50 50 50 0.8 units/{civ}_support_female_citizen phase_town phase_city food wood stone metal interface/complete/building/complete_civ_center.xml attack/weapon/arrowfly.xml attack/destruction/building_collapse_large.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml interface/alarm/alarm_alert_2.xml true 140 10000 2 140 90 structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 17407) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 17408) @@ -1,130 +1,131 @@ 1 1 15 1 0 1 0 0 0 0 false false 80.0 0.01 0.0 2.5 corpse 100 0 + false false Unit Unit ConquestCritical formations/null formations/box formations/column_closed formations/line_closed formations/column_open formations/line_open formations/flank formations/battle_line unit true true false false true false false 2.0 1.0 1 10 10 10 10 circle/128x128.png circle/128x128_mask.png interface/alarm/alarm_attackplayer.xml 2.0 0.333 5.0 2 aggressive 12.0 false true false 9 15.0 50.0 0.0 0.1 0.2 default false false false false 12 true false false