Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 11441) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 11442) @@ -1,1419 +1,1419 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; const SDLK_LEFTBRACKET = 91; const SDLK_RIGHTBRACKET = 93; const SDLK_RSHIFT = 303; const SDLK_LSHIFT = 304; const SDLK_RCTRL = 305; const SDLK_LCTRL = 306; const SDLK_RALT = 307; const SDLK_LALT = 308; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too const ACTION_NONE = 0; const ACTION_GARRISON = 1; const ACTION_REPAIR = 2; var preSelectedAction = ACTION_NONE; var INPUT_NORMAL = 0; var INPUT_SELECTING = 1; var INPUT_BANDBOXING = 2; var INPUT_BUILDING_PLACEMENT = 3; var INPUT_BUILDING_CLICK = 4; var INPUT_BUILDING_DRAG = 5; var INPUT_BATCHTRAINING = 6; var INPUT_PRESELECTEDACTION = 7; var inputState = INPUT_NORMAL; var defaultPlacementAngle = Math.PI*3/4; var placementAngle = undefined; var placementPosition = undefined; var placementEntity = undefined; var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; // Number of pixels the mouse can move before the action is considered a drag var maxDragDelta = 4; // Time in milliseconds in which a double click is recognized const doubleClickTime = 500; var doubleClickTimer = 0; var doubleClicked = false; // Store the previously clicked entity - ensure a double/triple click happens on the same entity var prevClickedEntity = 0; // Same double-click behaviour for hotkey presses const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursorAndTooltip() { var cursorSet = false; var tooltipSet = false; var informationTooltip = getGUIObjectByName("informationTooltip"); if (!mouseIsOverObject) { var action = determineAction(mouseX, mouseY); if (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) { if (action) { if (action.cursor) { Engine.SetCursor(action.cursor); cursorSet = true; } if (action.tooltip) { tooltipSet = true; informationTooltip.caption = action.tooltip; informationTooltip.hidden = false; } } } } if (!cursorSet) Engine.SetCursor("arrow-default"); if (!tooltipSet) informationTooltip.hidden = true; } function updateBuildingPlacementPreview() { // The preview should be recomputed every turn, so that it responds // to obstructions/fog/etc moving underneath it if (placementEntity && placementPosition) { return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementEntity, "x": placementPosition.x, "z": placementPosition.z, "angle": placementAngle }); } return false; } function resetPlacementEntity() { Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""}); placementEntity = undefined; placementPosition = undefined; placementAngle = undefined; } function findGatherType(gatherer, supply) { if (!gatherer || !supply) return undefined; if (gatherer[supply.type.generic+"."+supply.type.specific]) return supply.type.specific; if (gatherer[supply.type.generic]) return supply.type.generic; return undefined; } function getActionInfo(action, target) { var selection = g_Selection.toList(); // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return {"possible": false}; // If the selection isn't friendly units, no action var playerID = Engine.GetPlayerID(); var allOwnedByPlayer = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.player == playerID; }); if (!g_DevSettings.controlAll && !allOwnedByPlayer) return {"possible": false}; // Work out whether the selection can have rally points var haveRallyPoints = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.rallyPoint; }); if (!target) { if (action == "set-rallypoint" && haveRallyPoints) return {"possible": true}; else if (action == "move") return {"possible": true}; else return {"possible": false}; } if (haveRallyPoints && selection.indexOf(target) != -1 && action == "unset-rallypoint") return {"possible": true}; // Look at the first targeted entity // (TODO: maybe we eventually want to look at more, and be more context-sensitive? // e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse) var targetState = GetEntityState(target); // If we selected buildings with rally points, and then click on one of those selected // buildings, we should remove the rally point //if (haveRallyPoints && selection.indexOf(target) != -1) // return {"type": "unset-rallypoint"}; // Check if the target entity is a resource, dropsite, foundation, or enemy unit. // Check if any entities in the selection can gather the requested resource, // can return to the dropsite, can build the foundation, or can attack the enemy var simState = Engine.GuiInterfaceCall("GetSimulationState"); // Look to see what type of command units going to the rally point should use if (haveRallyPoints && action == "set-rallypoint") { // haveRallyPoints ensures all selected entities can have rally points. // We assume that all entities are owned by the same player. var entState = GetEntityState(selection[0]); var playerState = simState.players[entState.player]; var playerOwned = (targetState.player == entState.player); var allyOwned = playerState.isAlly[targetState.player]; var enemyOwned = playerState.isEnemy[targetState.player]; var gaiaOwned = (targetState.player == 0); var cursor = ""; // default to walking there var data = {command: "walk"}; if (targetState.garrisonHolder && playerOwned) { // Don't allow the rally point to be set on any of the currently selected units for (var i = 0; i < selection.length; i++) { if (target === selection[i]) { return {"possible": false}; } } data.command = "garrison"; data.target = target; cursor = "action-garrison"; } else if (targetState.resourceSupply && (playerOwned || gaiaOwned)) { var resourceType = targetState.resourceSupply.type.specific; if (targetState.resourceSupply.type.generic === "treasure") { cursor = "action-gather-" + targetState.resourceSupply.type.generic; } else { cursor = "action-gather-" + targetState.resourceSupply.type.specific; } data.command = "gather"; data.resourceType = resourceType; } else if (targetState.foundation && entState.buildEntities) { data.command = "build"; data.target = target; cursor = "action-build"; } else if (targetState.needsRepair && allyOwned) { data.command = "repair"; data.target = target; cursor = "action-repair"; } return {"possible": true, "data": data, "position": targetState.position, "cursor": cursor}; } for each (var entityID in selection) { var entState = GetEntityState(entityID); if (!entState) continue; var playerState = simState.players[entState.player]; var playerOwned = (targetState.player == entState.player); var allyOwned = playerState.isAlly[targetState.player]; var enemyOwned = playerState.isEnemy[targetState.player]; var gaiaOwned = (targetState.player == 0); // Find the resource type we're carrying, if any var carriedType = undefined; if (entState.resourceCarrying && entState.resourceCarrying.length) carriedType = entState.resourceCarrying[0].type; switch (action) { case "garrison": if (hasClass(entState, "Unit") && targetState.garrisonHolder && playerOwned) { var allowedClasses = targetState.garrisonHolder.allowedClasses; for each (var unitClass in entState.identity.classes) { if (allowedClasses.indexOf(unitClass) != -1) { return {"possible": true}; } } } break; case "setup-trade-route": // If ground or sea trade possible if (!targetState.foundation && ((entState.trader && hasClass(entState, "Organic") && (playerOwned || allyOwned) && hasClass(targetState, "Market")) || (entState.trader && hasClass(entState, "Ship") && (playerOwned || allyOwned) && hasClass(targetState, "NavalMarket")))) { var tradingData = {"trader": entState.id, "target": target}; var tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", tradingData); var tooltip; if (tradingDetails === null) return {"possible": false}; switch (tradingDetails.type) { case "is first": tooltip = "First trade market."; if (tradingDetails.hasBothMarkets) tooltip += " Gain: " + tradingDetails.gain + " " + tradingDetails.goods + ". Click to establish another route." else tooltip += " Click on another market to establish a trade route." break; case "is second": tooltip = "Second trade market. Gain: " + tradingDetails.gain + " " + tradingDetails.goods + "." + " Click to establish another route."; break; case "set first": tooltip = "Set as first trade market"; break; case "set second": tooltip = "Set as second trade market. Gain: " + tradingDetails.gain + " " + tradingDetails.goods + "."; break; } return {"possible": true, "tooltip": tooltip}; } break; case "gather": if (targetState.resourceSupply && (playerOwned || gaiaOwned)) { var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply); if (resource) return {"possible": true, "cursor": "action-gather-" + resource}; } break; case "returnresource": if (targetState.resourceDropsite && playerOwned && carriedType && targetState.resourceDropsite.types.indexOf(carriedType) != -1) return {"possible": true, "cursor": "action-return-" + carriedType}; break; case "build": if (targetState.foundation && entState.buildEntities && playerOwned) return {"possible": true}; break; case "repair": if (entState.buildEntities && targetState.needsRepair && allyOwned) return {"possible": true}; break; case "attack": if (entState.attack && targetState.hitpoints && enemyOwned) return {"possible": true}; break; } } if (action == "move") return {"possible": true}; else return {"possible": false}; } /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ function determineAction(x, y, fromMinimap) { var selection = g_Selection.toList(); // No action if there's no selection if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return undefined; // If the selection isn't friendly units, no action var playerID = Engine.GetPlayerID(); var allOwnedByPlayer = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.player == playerID; }); if (!g_DevSettings.controlAll && !allOwnedByPlayer) return undefined; // Work out whether the selection can have rally points var haveRallyPoints = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.rallyPoint; }); var targets = []; var target = undefined; var type = "none"; var cursor = ""; var targetState = undefined; if (!fromMinimap) targets = Engine.PickEntitiesAtPoint(x, y); if (targets.length) { target = targets[0]; } if (preSelectedAction != ACTION_NONE) { switch (preSelectedAction) { case ACTION_GARRISON: if (getActionInfo("garrison", target).possible) return {"type": "garrison", "cursor": "action-garrison", "target": target}; else return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined}; break; case ACTION_REPAIR: if (getActionInfo("repair", target).possible) return {"type": "repair", "cursor": "action-repair", "target": target}; else return {"type": "none", "cursor": "action-repair-disabled", "target": undefined}; break; } } else if (Engine.HotkeyIsPressed("session.garrison")) { if (getActionInfo("garrison", target).possible) return {"type": "garrison", "cursor": "action-garrison", "target": target}; else return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined}; } else { var actionInfo = undefined; if ((actionInfo = getActionInfo("setup-trade-route", target)).possible) return {"type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target}; else if ((actionInfo = getActionInfo("gather", target)).possible) return {"type": "gather", "cursor": actionInfo.cursor, "target": target}; else if ((actionInfo = getActionInfo("returnresource", target)).possible) return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target}; else if (getActionInfo("build", target).possible) return {"type": "build", "cursor": "action-build", "target": target}; else if (getActionInfo("repair", target).possible) return {"type": "build", "cursor": "action-repair", "target": target}; else if ((actionInfo = getActionInfo("set-rallypoint", target)).possible) return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "position": actionInfo.position}; else if (getActionInfo("attack", target).possible) return {"type": "attack", "cursor": "action-attack", "target": target}; else if (getActionInfo("unset-rallypoint", target).possible) return {"type": "unset-rallypoint"}; else if (getActionInfo("move", target).possible) return {"type": "move"}; } return {"type": type, "cursor": cursor, "target": target}; } var dragStart; // used for remembering mouse coordinates at start of drag operations function tryPlaceBuilding(queued) { var selection = g_Selection.toList(); // Use the preview to check it's a valid build location if (!updateBuildingPlacementPreview()) { // invalid location - don't build it // TODO: play a sound? return false; } // Start the construction Engine.PostNetworkCommand({ "type": "construct", "template": placementEntity, "x": placementPosition.x, "z": placementPosition.z, "angle": placementAngle, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); if (!queued) resetPlacementEntity(); return true; } // Limits bandboxed selections to certain types of entities based on priority function getPreferredEntities(ents) { var entStateList = []; var preferredEnts = []; // Check if there are units in the selection and get a list of entity states for each (var ent in ents) { var entState = GetEntityState(ent); if (!entState) continue; if (hasClass(entState, "Unit")) preferredEnts.push(ent); entStateList.push(entState); } // If there are no units, check if there are defensive entities in the selection if (!preferredEnts.length) for (var i = 0; i < ents.length; i++) if (hasClass(entStateList[i], "Defensive")) preferredEnts.push(ents[i]); return preferredEnts; } // Removes any support units from the passed list of entities function getMilitaryEntities(ents) { var militaryEnts = []; for each (var ent in ents) { var entState = GetEntityState(ent); if (!hasClass(entState, "Support")) militaryEnts.push(ent); } return militaryEnts; } function handleInputBeforeGui(ev, hoveredObject) { // Capture mouse position so we can use it for displaying cursors, // and key states switch (ev.type) { case "mousebuttonup": case "mousebuttondown": case "mousemotion": mouseX = ev.x; mouseY = ev.y; break; } // Remember whether the mouse is over a GUI object or not mouseIsOverObject = (hoveredObject != null); // State-machine processing: // // (This is for states which should override the normal GUI processing - events will // be processed here before being passed on, and propagation will stop if this function // returns true) // // TODO: it'd probably be nice to have a better state-machine system, with guaranteed // entry/exit functions, since this is a bit broken now switch (inputState) { case INPUT_BANDBOXING: switch (ev.type) { case "mousemotion": var x0 = dragStart[0]; var y0 = dragStart[1]; var x1 = ev.x; var y1 = ev.y; if (x0 > x1) { var t = x0; x0 = x1; x1 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; } var bandbox = getGUIObjectByName("bandbox"); bandbox.size = [x0, y0, x1, y1].join(" "); bandbox.hidden = false; // TODO: Should we handle "control all units" here as well? var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID()); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var x0 = dragStart[0]; var y0 = dragStart[1]; var x1 = ev.x; var y1 = ev.y; if (x0 > x1) { var t = x0; x0 = x1; x1 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; } var bandbox = getGUIObjectByName("bandbox"); bandbox.hidden = true; // Get list of entities limited to preferred entities // TODO: Should we handle "control all units" here as well? var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID()); var preferredEntities = getPreferredEntities(ents) if (preferredEntities.length) { ents = preferredEntities; if (Engine.HotkeyIsPressed("selection.milonly")) { var militaryEntities = getMilitaryEntities(ents); if (militaryEntities.length) ents = militaryEntities; } } // Remove the bandbox hover highlighting g_Selection.setHighlightList([]); // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel selection var bandbox = getGUIObjectByName("bandbox"); bandbox.hidden = true; g_Selection.setHighlightList([]); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_CLICK: switch (ev.type) { case "mousemotion": // If the mouse moved far enough from the original click location, // then switch to drag-orientation mode var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; var maxDragDelta = 16; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { inputState = INPUT_BUILDING_DRAG; return false; } break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building resetPlacementEntity(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_DRAG: switch (ev.type) { case "mousemotion": var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; var maxDragDelta = 16; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { // Rotate in the direction of the mouse var target = Engine.GetTerrainAtPoint(ev.x, ev.y); placementAngle = Math.atan2(target.x - placementPosition.x, target.z - placementPosition.z); } else { // If the mouse is near the center, snap back to the default orientation placementAngle = defaultPlacementAngle; } var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementEntity, "x": placementPosition.x, "z": placementPosition.z }); if (snapData) { placementAngle = snapData.angle; placementPosition.x = snapData.x; placementPosition.z = snapData.z; } updateBuildingPlacementPreview(); break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building resetPlacementEntity(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BATCHTRAINING: switch (ev.type) { case "hotkeyup": if (ev.hotkey == "session.batchtrain") { flushTrainingQueueBatch(); inputState = INPUT_NORMAL; } break; } } return false; } function handleInputAfterGui(ev) { // Handle the time-warp testing features, restricted to single-player if (!g_IsNetworked && getGUIObjectByName("devTimeWarp").checked) { if (ev.type == "hotkeydown" && ev.hotkey == "timewarp.fastforward") Engine.SetSimRate(20.0); else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.fastforward") Engine.SetSimRate(1.0); else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.rewind") Engine.RewindTimeWarp(); } // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": // Highlight the first hovered entity (if any) var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (ents.length) g_Selection.setHighlightList([ents[0]]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { dragStart = [ ev.x, ev.y ]; inputState = INPUT_SELECTING; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { var action = determineAction(ev.x, ev.y); if (!action) break; return doAction(action, ev); } break; case "hotkeydown": if (ev.hotkey.indexOf("selection.group.") == 0) { var now = new Date(); if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey)) { if (ev.hotkey.indexOf("selection.group.select.") == 0) { var sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { var sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now.getTime(); prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) { var action = determineAction(ev.x, ev.y); if (!action) break; preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; return doAction(action, ev); } else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } // else default: // Slight hack: If selection is empty, reset the input state if (g_Selection.toList().length == 0) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": // If the mouse moved further than a limit, switch to bandbox mode var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { inputState = INPUT_BANDBOXING; return false; } var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (!ents.length) { g_Selection.reset(); resetIdleUnit(); inputState = INPUT_NORMAL; return true; } var selectedEntity = ents[0]; var now = new Date(); // If camera following and we select different unit, stop if (Engine.GetFollowedEntity() != selectedEntity) { Engine.CameraFollow(0); } if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity)) { // Double click or triple click has occurred var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); var matchRank = true; var templateToMatch; // Check for double click or triple click if (!doubleClicked) { // If double click hasn't already occurred, this is a double click. // Select similar units regardless of rank templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).identity.selectionGroupName; if (templateToMatch) { matchRank = false; } else { // No selection group name defined, so fall back to exact match templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).template; } doubleClicked = true; // Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click doubleClickTimer = now.getTime(); } else { // Double click has already occurred, so this is a triple click. // Select units matching exact template name (same rank) templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).template; } // TODO: Should we handle "control all units" here as well? ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank); } else { // It's single click right now but it may become double or triple click doubleClicked = false; doubleClickTimer = now.getTime(); prevClickedEntity = selectedEntity; // We only want to include the first picked unit in the selection ents = [ents[0]]; } // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_PLACEMENT: switch (ev.type) { case "mousemotion": placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y); var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementEntity, "x": placementPosition.x, "z": placementPosition.z }); if (snapData) { placementAngle = snapData.angle; placementPosition.x = snapData.x; placementPosition.z = snapData.z; } updateBuildingPlacementPreview(); return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y); dragStart = [ ev.x, ev.y ]; inputState = INPUT_BUILDING_CLICK; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building resetPlacementEntity(); inputState = INPUT_NORMAL; return true; } break; case "hotkeydown": var rotation_step = Math.PI / 12; // 24 clicks make a full rotation switch (ev.hotkey) { case "session.rotate.cw": placementAngle += rotation_step; updateBuildingPlacementPreview(); break; case "session.rotate.ccw": placementAngle -= rotation_step; updateBuildingPlacementPreview(); break; } break; } break; } return false; } function doAction(action, ev) { var selection = g_Selection.toList(); // If shift is down, add the order to the unit's order queue instead // of running it immediately var queued = Engine.HotkeyIsPressed("session.queue"); switch (action.type) { case "move": var target = Engine.GetTerrainAtPoint(ev.x, ev.y); 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; case "attack": Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); return true; case "build": // (same command as repair) case "repair": Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); return true; case "gather": Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; case "returnresource": Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; case "setup-trade-route": Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target}); return true; case "garrison": Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued}); // TODO: Play a sound? return true; case "set-rallypoint": var pos = undefined; // 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) { pos = action.position; } else { pos = Engine.GetTerrainAtPoint(ev.x, ev.y); } Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": pos.x, "z": pos.z, "data": action.data}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": pos.x, "z": pos.z }); return true; case "unset-rallypoint": var target = Engine.GetTerrainAtPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection}); // Remove displayed rally point Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": [] }); return true; case "none": return true; default: error("Invalid action.type "+action.type); return false; } } function handleMinimapEvent(target) { // Partly duplicated from handleInputAfterGui(), but with the input being // world coordinates instead of screen coordinates. if (inputState == INPUT_NORMAL) { var fromMinimap = true; var action = determineAction(undefined, undefined, fromMinimap); if (!action) return false; var selection = g_Selection.toList(); var queued = Engine.HotkeyIsPressed("session.queue"); switch (action.type) { case "move": 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; case "set-rallypoint": Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z }); return true; default: error("Invalid action.type "+action.type); } } return false; } // Called by GUI when user clicks construction button function startBuildingPlacement(buildEntType) { placementEntity = buildEntType; placementAngle = defaultPlacementAngle; inputState = INPUT_BUILDING_PLACEMENT; } // Called by GUI when user changes preferred trading goods function selectTradingPreferredGoods(data) { - Engine.PostNetworkCommand({"type": "select-trading-goods", "trader": data.trader, "preferredGoods": data.preferredGoods}); + Engine.PostNetworkCommand({"type": "select-trading-goods", "entities": data.entities, "preferredGoods": data.preferredGoods}); } // Called by GUI when user clicks exchange resources button function exchangeResources(command) { Engine.PostNetworkCommand({"type": "barter", "sell": command.sell, "buy": command.buy, "amount": command.amount}); } // Batch training: // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING // When the user releases shift, or clicks on a different training button, we create the batched units var batchTrainingEntity; var batchTrainingType; var batchTrainingCount; const batchIncrementSize = 5; function flushTrainingQueueBatch() { Engine.PostNetworkCommand({"type": "train", "entity": batchTrainingEntity, "template": batchTrainingType, "count": batchTrainingCount}); } // Called by GUI when user clicks training button function addToTrainingQueue(entity, trainEntType) { if (Engine.HotkeyIsPressed("session.batchtrain")) { if (inputState == INPUT_BATCHTRAINING) { // If we're already creating a batch of this unit, then just extend it if (batchTrainingEntity == entity && batchTrainingType == trainEntType) { batchTrainingCount += batchIncrementSize; return; } // Otherwise start a new one else { flushTrainingQueueBatch(); // fall through to create the new batch } } inputState = INPUT_BATCHTRAINING; batchTrainingEntity = entity; batchTrainingType = trainEntType; batchTrainingCount = batchIncrementSize; } else { // Non-batched - just create a single entity Engine.PostNetworkCommand({"type": "train", "entity": entity, "template": trainEntType, "count": 1}); } } // Returns the number of units that will be present in a batch if the user clicks // the training button with shift down function getTrainingQueueBatchStatus(entity, trainEntType) { if (inputState == INPUT_BATCHTRAINING && batchTrainingEntity == entity && batchTrainingType == trainEntType) return [batchTrainingCount, batchIncrementSize]; else return [0, batchIncrementSize]; } // Called by GUI when user clicks production queue item function removeFromTrainingQueue(entity, id) { Engine.PostNetworkCommand({"type": "stop-train", "entity": entity, "id": id}); } // Called by unit selection buttons function changePrimarySelectionGroup(templateName) { if (Engine.HotkeyIsPressed("session.deselectgroup")) g_Selection.makePrimarySelection(templateName, true); else g_Selection.makePrimarySelection(templateName, false); } // Performs the specified command (delete, town bell, repair, etc.) function performCommand(entity, commandName) { if (entity) { var entState = GetEntityState(entity); var template = GetTemplateData(entState.template); var unitName = getEntityName(template); var playerID = Engine.GetPlayerID(); if (entState.player == playerID || g_DevSettings.controlAll) { switch (commandName) { case "delete": var selection = g_Selection.toList(); if (selection.length > 0) openDeleteDialog(selection); break; case "garrison": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; break; case "repair": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; break; case "unload-all": unloadAll(entity); break; case "focus-rally": // if the selected building has a rally point set, move the camera to it; otherwise, move to the building itself // (since that's where units will spawn without a rally point) var focusTarget = null; if (entState.rallyPoint && entState.rallyPoint.position) { focusTarget = entState.rallyPoint.position; } else { if (entState.position) focusTarget = entState.position; } if (focusTarget !== null) Engine.CameraMoveTo(focusTarget.x, focusTarget.z); break; default: break; } } } } // Performs the specified formation function performFormation(entity, formationName) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "formation", "entities": selection, "name": formationName }); } } // Performs the specified group function performGroup(action, groupId) { switch (action) { case "snap": case "select": var toSelect = []; g_Groups.update(); for (var ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) Engine.CameraFollow(toSelect[0]); break; case "add": var selection = g_Selection.toList(); g_Groups.addEntities(groupId, selection); updateGroups(); break; case "save": var selection = g_Selection.toList(); g_Groups.groups[groupId].reset(); g_Groups.addEntities(groupId, selection); updateGroups(); break; } } // Performs the specified stance function performStance(entity, stanceName) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "stance", "entities": selection, "name": stanceName }); } } // Set the camera to follow the given unit function setCameraFollow(entity) { // Follow the given entity if it's a unit if (entity) { var entState = GetEntityState(entity); if (entState && hasClass(entState, "Unit")) { Engine.CameraFollow(entity); return; } } // Otherwise stop following Engine.CameraFollow(0); } var lastIdleUnit = 0; var currIdleClass = 0; function resetIdleUnit() { lastIdleUnit = 0; currIdleClass = 0; } function findIdleUnit(classes) { // Cycle through idling classes before giving up for (var i = 0; i <= classes.length; ++i) { var data = { prevUnit: lastIdleUnit, idleClass: classes[currIdleClass] }; var newIdleUnit = Engine.GuiInterfaceCall("FindIdleUnit", data); // Check if we have new valid entity if (newIdleUnit && newIdleUnit != lastIdleUnit) { lastIdleUnit = newIdleUnit; g_Selection.reset() g_Selection.addList([lastIdleUnit]); Engine.CameraFollow(lastIdleUnit); return; } lastIdleUnit = 0; currIdleClass = (currIdleClass + 1) % classes.length; } // TODO: display a message or play a sound to indicate no more idle units, or something // Reset for next cycle resetIdleUnit(); } function unload(garrisonHolder, entities) { if (Engine.HotkeyIsPressed("session.unloadtype")) Engine.PostNetworkCommand({"type": "unload", "entities": entities, "garrisonHolder": garrisonHolder}); else Engine.PostNetworkCommand({"type": "unload", "entities": [entities[0]], "garrisonHolder": garrisonHolder}); } function unloadAll(garrisonHolder) { Engine.PostNetworkCommand({"type": "unload-all", "garrisonHolder": garrisonHolder}); } Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 11441) +++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 11442) @@ -1,607 +1,607 @@ // Panel types const SELECTION = "Selection"; const QUEUE = "Queue"; const GARRISON = "Garrison"; const FORMATION = "Formation"; const TRAINING = "Training"; const CONSTRUCTION = "Construction"; const COMMAND = "Command"; const STANCE = "Stance"; // Constants const COMMANDS_PANEL_WIDTH = 228; const UNIT_PANEL_BASE = -52; // QUEUE: The offset above the main panel (will often be negative) const UNIT_PANEL_HEIGHT = 44; // QUEUE: The height needed for a row of buttons // Trading constants const TRADING_RESOURCES = ["food", "wood", "stone", "metal"]; // Barter constants const BARTER_RESOURCE_AMOUNT_TO_SELL = 100; const BARTER_BUNCH_MULTIPLIER = 5; const BARTER_RESOURCES = ["food", "wood", "stone", "metal"]; const BARTER_ACTIONS = ["Sell", "Buy"]; // The number of currently visible buttons (used to optimise showing/hiding) var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Barter": 0, "Trading": 0, "Construction": 0, "Command": 0, "Stance": 0}; // Unit panels are panels with row(s) of buttons var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Training", "Barter", "Trading", "Construction", "Research", "Stance", "Command"]; // Indexes of resources to sell and buy on barter panel var g_barterSell = 0; var g_barterBuy = 1; // Lay out a row of centered buttons (does not work inside a loop like the other function) function layoutButtonRowCentered(rowNumber, guiName, startIndex, endIndex, width) { var buttonSideLength = getGUIObjectByName("unit"+guiName+"Button[0]").size.bottom; var buttonSpacer = buttonSideLength+1; var colNumber = 0; // Collect buttons var buttons = []; var icons = []; for (var i = startIndex; i < endIndex; i++) { var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]"); var icon = getGUIObjectByName("unit"+guiName+"Icon["+i+"]"); if (button) { buttons.push(button); icons.push(icon); } } // Location of middle button var middleIndex = Math.ceil(buttons.length/2); // Determine whether even or odd number of buttons var center = (buttons.length/2 == Math.ceil(buttons.length/2))? Math.ceil(width/2) : Math.ceil(width/2+buttonSpacer/2); // Left Side for (var i = middleIndex-1; i >= 0; i--) { if (buttons[i]) { var icon = icons[i]; var size = buttons[i].size; size.left = center - buttonSpacer*colNumber - buttonSideLength; size.right = center - buttonSpacer*colNumber; size.top = buttonSpacer*rowNumber; size.bottom = buttonSpacer*rowNumber + buttonSideLength; buttons[i].size = size; colNumber++; } } // Right Side center += 1; // add spacing to center buttons colNumber = 0; // reset to 0 for (var i = middleIndex; i < buttons.length; i++) { if (buttons[i]) { var icon = icons[i]; var size = buttons[i].size; size.left = center + buttonSpacer*colNumber; size.right = center + buttonSpacer*colNumber + buttonSideLength; size.top = buttonSpacer*rowNumber; size.bottom = buttonSpacer*rowNumber + buttonSideLength; buttons[i].size = size; colNumber++; } } } // Lay out button rows function layoutButtonRow(rowNumber, guiName, buttonSideLength, buttonSpacer, startIndex, endIndex) { var colNumber = 0; for (var i = startIndex; i < endIndex; i++) { var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]"); if (button) { var size = button.size; size.left = buttonSpacer*colNumber; size.right = buttonSpacer*colNumber + buttonSideLength; size.top = buttonSpacer*rowNumber; size.bottom = buttonSpacer*rowNumber + buttonSideLength; button.size = size; colNumber++; } } } function selectBarterResourceToSell(resourceIndex) { g_barterSell = resourceIndex; // g_barterBuy should be set to different value in case if it is the same as g_barterSell // (it is no make sense to exchange resource to the same one). // We change it cyclic to next value. if (g_barterBuy == g_barterSell) g_barterBuy = (g_barterBuy + 1) % BARTER_RESOURCES.length; } // Sets up "unit panels" - the panels with rows of icons (Helper function for updateUnitDisplay) function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback) { usedPanels[guiName] = 1; var numberOfItems = items.length; var selection = g_Selection.toList(); var garrisonGroups = new EntityGroups(); // Determine how many buttons there should be switch (guiName) { case SELECTION: if (numberOfItems > 16) numberOfItems = 16; break; case QUEUE: if (numberOfItems > 16) numberOfItems = 16; break; case GARRISON: if (numberOfItems > 16) numberOfItems = 16; //Group garrisoned units based on class garrisonGroups.add(unitEntState.garrisonHolder.entities); break; case STANCE: if (numberOfItems > 5) numberOfItems = 5; case FORMATION: if (numberOfItems > 16) numberOfItems = 16; break; case TRAINING: if (numberOfItems > 24) numberOfItems = 24; break; case CONSTRUCTION: if (numberOfItems > 24) numberOfItems = 24; break; case COMMAND: if (numberOfItems > 6) numberOfItems = 6; break; default: break; } // Make buttons var i; for (i = 0; i < numberOfItems; i++) { var item = items[i]; var entType = ((guiName == "Queue")? item.template : item); var template; if (guiName != "Formation" && guiName != "Command" && guiName != "Stance") { template = GetTemplateData(entType); if (!template) continue; // ignore attempts to use invalid templates (an error should have been reported already) } switch (guiName) { case SELECTION: var name = getEntityName(template); var tooltip = name; var count = g_Selection.groups.getCount(item); getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 1 ? count : ""); break; case QUEUE: var tooltip = getEntityName(template); var progress = Math.round(item.progress*100) + "%"; getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (item.count > 1 ? item.count : ""); if (i == 0) { getGUIObjectByName("queueProgress").caption = (item.progress ? progress : ""); var size = getGUIObjectByName("unit"+guiName+"ProgressSlider["+i+"]").size; size.top = Math.round(item.progress*40); getGUIObjectByName("unit"+guiName+"ProgressSlider["+i+"]").size = size; } break; case GARRISON: var name = getEntityName(template); var tooltip = "Unload " + getEntityName(template) + "\nSingle-click to unload 1. Shift-click to unload all of this type."; var count = garrisonGroups.getCount(item); getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 1 ? count : ""); break; case STANCE: case FORMATION: var tooltip = toTitleCase(item); break; case TRAINING: var tooltip = getEntityNameWithGenericType(template); if (template.tooltip) tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]"; var [batchSize, batchIncrement] = getTrainingQueueBatchStatus(unitEntState.id, entType); var trainNum = batchSize ? batchSize+batchIncrement : batchIncrement; tooltip += "\n" + getEntityCost(template); if (template.health) tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health; if (template.armour) tooltip += "\n[font=\"serif-bold-13\"]Armour:[/font] " + damageTypesToText(template.armour); if (template.attack) tooltip += "\n" + getEntityAttack(template); if (template.speed) tooltip += "\n" + getEntitySpeed(template); tooltip += "\n\n[font=\"serif-bold-13\"]Shift-click[/font][font=\"serif-13\"] to train " + trainNum + ".[/font]"; break; case CONSTRUCTION: var tooltip = getEntityNameWithGenericType(template); if (template.tooltip) tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]"; tooltip += "\n" + getEntityCost(template); tooltip += getPopulationBonus(template); if (template.health) tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health; break; case COMMAND: // here, "item" is an object with properties .name (command name), .tooltip and .icon (relative to session/icons/single) if (item.name == "unload-all") { var count = unitEntState.garrisonHolder.entities.length; getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 0 ? count : ""); } else { getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = ""; } tooltip = (item.tooltip ? item.tooltip : toTitleCase(item.name)); break; default: break; } // Button var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]"); var icon = getGUIObjectByName("unit"+guiName+"Icon["+i+"]"); button.hidden = false; button.tooltip = tooltip; // Button Function (need nested functions to get the closure right) button.onpress = (function(e){ return function() { callback(e) } })(item); // Get icon image if (guiName == "Formation") { var formationOk = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", { "ents": g_Selection.toList(), "formationName": item }); button.enabled = formationOk; if (!formationOk) { icon.sprite = "stretched:session/icons/formations/formation-"+item.replace(/\s+/,'').toLowerCase()+".png"; // Display a meaningful tooltip why the formation is disabled var requirements = Engine.GuiInterfaceCall("GetFormationRequirements", { "formationName": item }); button.tooltip += " (disabled)"; if (requirements.count > 1) button.tooltip += "\n" + requirements.count + " units required"; if (requirements.classesRequired) { button.tooltip += "\nOnly units of type"; for each (var classRequired in requirements.classesRequired) { button.tooltip += " " + classRequired; } button.tooltip += " allowed."; } } else { var formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "ents": g_Selection.toList(), "formationName": item }); if (formationSelected) icon.sprite = "stretched:session/icons/formations/formation-"+item.replace(/\s+/,'').toLowerCase()+"-selected.png"; else icon.sprite = "stretched:session/icons/formations/formation-"+item.replace(/\s+/,'').toLowerCase()+"-available.png"; } } else if (guiName == "Stance") { var stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", { "ents": g_Selection.toList(), "stance": item }); if (stanceSelected) icon.sprite = "stretched:session/icons/single/stance-"+item+"-select.png"; else icon.sprite = "stretched:session/icons/single/stance-"+item+".png"; } else if (guiName == "Command") { icon.sprite = "stretched:session/icons/single/" + item.icon; } else if (template.icon) { icon.sprite = "stretched:session/portraits/" + template.icon; } else { // TODO: we should require all entities to have icons, so this case never occurs icon.sprite = "bkFillBlack"; } } // Position the visible buttons (TODO: if there's lots, maybe they should be squeezed together to fit) var numButtons = i; var rowLength = 8; if (guiName == "Selection") rowLength = 4; else if (guiName == "Formation" || guiName == "Garrison" || guiName == "Command") rowLength = 4; var numRows = Math.ceil(numButtons / rowLength); var buttonSideLength = getGUIObjectByName("unit"+guiName+"Button[0]").size.bottom; var buttonSpacer = buttonSideLength+1; // Layout buttons if (guiName == "Command") { layoutButtonRowCentered(0, guiName, 0, numButtons, COMMANDS_PANEL_WIDTH); } else { for (var i = 0; i < numRows; i++) layoutButtonRow(i, guiName, buttonSideLength, buttonSpacer, rowLength*i, rowLength*(i+1) ); } // Resize Queue panel if needed if (guiName == "Queue") // or garrison { var panel = getGUIObjectByName("unitQueuePanel"); var size = panel.size; size.top = (UNIT_PANEL_BASE - ((numRows-1)*UNIT_PANEL_HEIGHT)); panel.size = size; } // Hide any buttons we're no longer using for (i = numButtons; i < g_unitPanelButtons[guiName]; ++i) getGUIObjectByName("unit"+guiName+"Button["+i+"]").hidden = true; g_unitPanelButtons[guiName] = numButtons; } // Sets up "unit trading panel" - special case for setupUnitPanel -function setupUnitTradingPanel(unitEntState) +function setupUnitTradingPanel(unitEntState, selection) { for (var i = 0; i < TRADING_RESOURCES.length; i++) { var resource = TRADING_RESOURCES[i]; var button = getGUIObjectByName("unitTradingButton["+i+"]"); button.size = (i * 46) + " 0 " + ((i + 1) * 46) + " 46"; - var selectTradingPreferredGoodsData = { "trader": unitEntState.id, "preferredGoods": resource }; + var selectTradingPreferredGoodsData = { "entities": selection, "preferredGoods": resource }; button.onpress = (function(e){ return function() { selectTradingPreferredGoods(e); } })(selectTradingPreferredGoodsData); button.enabled = true; button.tooltip = "Set " + resource + " as trading goods"; var icon = getGUIObjectByName("unitTradingIcon["+i+"]"); var preferredGoods = unitEntState.trader.preferredGoods; var imageNameSuffix = (resource == preferredGoods) ? "selected" : "inactive"; icon.sprite = "stretched:session/resources/" + resource + "_" + imageNameSuffix + ".png"; } } // Sets up "unit barter panel" - special case for setupUnitPanel function setupUnitBarterPanel(unitEntState) { // Amount of player's resource to exchange var amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL; if (Engine.HotkeyIsPressed("session.massbarter")) amountToSell *= BARTER_BUNCH_MULTIPLIER; // One pass for each resource for (var i = 0; i < BARTER_RESOURCES.length; i++) { var resource = BARTER_RESOURCES[i]; // One pass for 'sell' row and another for 'buy' for (var j = 0; j < 2; j++) { var selectedResourceIndex = [g_barterSell, g_barterBuy][j]; var action = BARTER_ACTIONS[j]; var imageNameSuffix = (i == selectedResourceIndex) ? "selected" : "inactive"; var icon = getGUIObjectByName("unitBarter" + action + "Icon["+i+"]"); var button = getGUIObjectByName("unitBarter" + action + "Button["+i+"]"); button.size = (i * 46) + " 0 " + ((i + 1) * 46) + " 46"; var amountToBuy; // In 'buy' row show black icon in place corresponding to selected resource in 'sell' row if (j == 1 && i == g_barterSell) { button.enabled = false; button.tooltip = ""; icon.sprite = ""; amountToBuy = ""; } else { button.enabled = true; button.tooltip = action + " " + resource; icon.sprite = "stretched:session/resources/" + resource + "_" + imageNameSuffix + ".png"; var sellPrice = unitEntState.barterMarket.prices["sell"][BARTER_RESOURCES[g_barterSell]]; var buyPrice = unitEntState.barterMarket.prices["buy"][resource]; amountToBuy = "+" + Math.round(sellPrice / buyPrice * amountToSell); } var amount; if (j == 0) { button.onpress = (function(i){ return function() { selectBarterResourceToSell(i); } })(i); amount = (i == g_barterSell) ? "-" + amountToSell : ""; } else { button.onpress = (function(i){ return function() { g_barterBuy = i; } })(i); amount = amountToBuy; } getGUIObjectByName("unitBarter" + action + "Amount["+i+"]").caption = amount; } } var performDealButton = getGUIObjectByName("PerformDealButton"); var exchangeResourcesParameters = { "sell": BARTER_RESOURCES[g_barterSell], "buy": BARTER_RESOURCES[g_barterBuy], "amount": amountToSell }; performDealButton.onpress = function() { exchangeResources(exchangeResourcesParameters) }; } // Updates right Unit Commands Panel - runs in the main session loop via updateSelectionDetails() function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection) { //var isInvisible = true; // Panels that are active var usedPanels = {}; // If the selection is friendly units, add the command panels var player = Engine.GetPlayerID(); if (entState.player == player || g_DevSettings.controlAll) { if (entState.attack) // TODO - this should be based on some AI properties { //usedPanels["Stance"] = 1; //usedPanels["Formation"] = 1; // (These are disabled since they're not implemented yet) } else // TODO - this should be based on various other things { //usedPanels["Research"] = 1; } if (selection.length > 1) setupUnitPanel("Selection", usedPanels, entState, g_Selection.groups.getTemplateNames(), function (entType) { changePrimarySelectionGroup(entType); } ); var commands = getEntityCommandsList(entState); if (commands.length) setupUnitPanel("Command", usedPanels, entState, commands, function (item) { performCommand(entState.id, item.name); } ); if (entState.garrisonHolder) { var groups = new EntityGroups(); groups.add(entState.garrisonHolder.entities); setupUnitPanel("Garrison", usedPanels, entState, groups.getTemplateNames(), function (item) { unload(entState.id, groups.getEntsByName(item)); } ); } var formations = getEntityFormationsList(entState); if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && !entState.garrisonHolder && formations.length) { setupUnitPanel("Formation", usedPanels, entState, formations, function (item) { performFormation(entState.id, item); } ); } // TODO: probably should load the stance list from a data file, // and/or vary depending on what units are selected var stances = ["violent", "aggressive", "passive", "defensive", "standground"]; if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && !entState.garrisonHolder && stances.length) { setupUnitPanel("Stance", usedPanels, entState, stances, function (item) { performStance(entState.id, item); } ); } getGUIObjectByName("unitBarterPanel").hidden = !entState.barterMarket; if (entState.barterMarket) { usedPanels["Barter"] = 1; setupUnitBarterPanel(entState); } if (entState.buildEntities && entState.buildEntities.length) { setupUnitPanel("Construction", usedPanels, entState, entState.buildEntities, startBuildingPlacement); // isInvisible = false; } if (entState.training && entState.training.entities.length) { setupUnitPanel("Training", usedPanels, entState, entState.training.entities, function (trainEntType) { addToTrainingQueue(entState.id, trainEntType); } ); // isInvisible = false; } if (entState.training && entState.training.queue.length) setupUnitPanel("Queue", usedPanels, entState, entState.training.queue, function (item) { removeFromTrainingQueue(entState.id, item.id); } ); if (entState.trader) { usedPanels["Trading"] = 1; - setupUnitTradingPanel(entState); + setupUnitTradingPanel(entState, selection); } // supplementalDetailsPanel.hidden = false; // commandsPanel.hidden = isInvisible; } else { getGUIObjectByName("stamina").hidden = true; // supplementalDetailsPanel.hidden = true; // commandsPanel.hidden = true; } // Hides / unhides Unit Panels (panels should be grouped by type, not by order, but we will leave that for another time) var offset = 0; for each (var panelName in g_unitPanels) { var panel = getGUIObjectByName("unit" + panelName + "Panel"); if (usedPanels[panelName]) panel.hidden = false; else panel.hidden = true; } } // Force hide commands panels function hideUnitCommands() { for each (var panelName in g_unitPanels) getGUIObjectByName("unit" + panelName + "Panel").hidden = true; } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 11441) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 11442) @@ -1,708 +1,711 @@ // 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 cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerMan || player < 0) return; var playerEnt = cmpPlayerMan.GetPlayerByID(player); if (playerEnt == INVALID_ENTITY) return; var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); if (!cmpPlayer) return; var controlAllUnits = cmpPlayer.CanControlAllUnits(); // 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 switch (cmd.type) { case "debug-print": print(cmd.message); break; case "chat": var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": cmd.message}); break; case "control-all": cmpPlayer.SetControlAllUnits(cmd.flag); break; case "reveal-map": // Reveal the map for all players, not just the current player, // primarily to make it obvious to everyone that the player is cheating var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, cmd.enable); break; case "walk": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); }); break; case "attack": if (g_DebugCommands && !IsOwnedByEnemyOfPlayer(player, cmd.target)) { // This check is for debugging only! warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); } // See UnitAI.CanAttack for target checks var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Attack(cmd.target, cmd.queued); }); break; case "repair": // 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 var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); }); break; case "gather": 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 var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Gather(cmd.target, cmd.queued); }); break; case "gather-near-position": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.queued); }); break; case "returnresource": // 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 var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.ReturnResource(cmd.target, cmd.queued); }); break; case "train": // Verify that the building can be controlled by the player if (CanControlUnit(cmd.entity, player, controlAllUnits)) { var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue); if (queue) queue.AddBatch(cmd.template, +cmd.count, cmd.metadata); } else if (g_DebugCommands) { warn("Invalid command: training building cannot be controlled by player "+player+": "+uneval(cmd)); } break; case "stop-train": // Verify that the building can be controlled by the player if (CanControlUnit(cmd.entity, player, controlAllUnits)) { var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue); if (queue) queue.RemoveBatch(cmd.id); } else if (g_DebugCommands) { warn("Invalid command: training building cannot be controlled by player "+player+": "+uneval(cmd)); } break; case "construct": // Message structure: // { // "type": "construct", // "entities": [...], // "template": "...", // "x": ..., // "z": ..., // "angle": ..., // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // } /* * 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 that we can control these units var entities = FilterEntityList(cmd.entities, player, controlAllUnits); if (!entities.length) break; // Tentatively create the foundation (we might find later that it's a invalid build command) var ent = Engine.AddEntity("foundation|" + cmd.template); if (ent == INVALID_ENTITY) { // Error (e.g. invalid template names) error("Error creating foundation entity for '" + cmd.template + "'"); break; } // Move the foundation to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(cmd.angle); // Check whether it's obstructed by other entities or invalid terrain var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player)) { if (g_DebugCommands) { warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd)); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" }); // Remove the foundation because the construction was aborted Engine.DestroyEntity(ent); break; } // Check build limits var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits); if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) { if (g_DebugCommands) { warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd)); } // TODO: The UI should tell the user they can't build this (but we still need this check) // Remove the foundation because the construction was aborted Engine.DestroyEntity(ent); break; } // TODO: AI has no visibility info if (!cmpPlayer.IsAI()) { // Check whether it's in a visible or fogged region // tell GetLosVisibility to force RetainInFog because preview entities set this to false, // which would show them as hidden instead of fogged var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden"); if (!visible) { if (g_DebugCommands) { warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd)); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" }); Engine.DestroyEntity(ent); break; } } var cmpCost = Engine.QueryInterface(ent, IID_Cost); if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts())) { if (g_DebugCommands) { warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd)); } Engine.DestroyEntity(ent); break; } // Make it owned by the current player var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Initialise the foundation var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); cmpFoundation.InitialiseConstruction(player, cmd.template); // 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 }); } break; case "delete-entities": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) { var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(ent); } break; case "set-rallypoint": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { cmpRallyPoint.SetPosition(cmd.x, cmd.z); cmpRallyPoint.SetData(cmd.data); } } break; case "unset-rallypoint": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Unset(); } break; case "defeat-player": // Send "OnPlayerDefeated" message to player Engine.PostMessage(playerEnt, MT_PlayerDefeated, { "playerId": player } ); break; case "garrison": // Verify that the building can be controlled by the player if (CanControlUnit(cmd.target, player, controlAllUnits)) { var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Garrison(cmd.target); }); } else if (g_DebugCommands) { warn("Invalid command: garrison target cannot be controlled by player "+player+": "+uneval(cmd)); } break; case "unload": // Verify that the building can be controlled by the player if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits)) { var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); var notUngarrisoned = 0; for each (ent in cmd.entities) { if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent)) { notUngarrisoned++; } } if (notUngarrisoned != 0) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var notification = {"player": cmpPlayer.GetPlayerID(), "message": (notUngarrisoned == 1 ? "Unable to ungarrison unit" : "Unable to ungarrison units")}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } } else if (g_DebugCommands) { warn("Invalid command: unload target cannot be controlled by player "+player+": "+uneval(cmd)); } break; case "unload-all": // Verify that the building can be controlled by the player if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits)) { var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll()) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Unable to ungarrison all units"}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } } else if (g_DebugCommands) { warn("Invalid command: unload-all target cannot be controlled by player "+player+": "+uneval(cmd)); } break; case "formation": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { var cmpFormation = Engine.QueryInterface(cmpUnitAI.entity, IID_Formation); if (!cmpFormation) return; cmpFormation.LoadFormation(cmd.name); cmpFormation.MoveMembersIntoFormation(true); }); break; case "promote": // No need to do checks here since this is a cheat anyway var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "chat", "player": 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()); } break; case "stance": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SwitchToStance(cmd.name); } break; case "setup-trade-route": for each (var ent in cmd.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SetupTradeRoute(cmd.target); } break; case "select-trading-goods": - var cmpTrader = Engine.QueryInterface(cmd.trader, IID_Trader); - if (cmpTrader) - cmpTrader.SetPreferredGoods(cmd.preferredGoods); + for each (var ent in cmd.entities) + { + var cmpTrader = Engine.QueryInterface(ent, IID_Trader); + if (cmpTrader) + cmpTrader.SetPreferredGoods(cmd.preferredGoods); + } break; case "barter": var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); cmpBarter.ExchangeResources(playerEnt, cmd.sell, cmd.buy, cmd.amount); break; default: error("Invalid command: unknown command type: "+uneval(cmd)); } } /** * 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 }; } /** * 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) { // 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 var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: Currently we use LineClosed as effectively a boolean flag // to determine whether formations are allowed at all. Instead we // should check specific formation names and do something sensible // (like what?) when some units don't support them. // TODO: We'll also need to fix other formation code to use // "LineClosed" instead of "Line Closed" etc consistently. if (cmpIdentity && cmpIdentity.CanUseFormation("LineClosed")) formedEnts.push(ent); else 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 formationEnt = undefined; 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) { // The whole formation was selected, so reuse its controller for this command formationEnt = +fid; } } if (!formationEnt) { // 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]); } // Create the new controller formationEnt = Engine.AddEntity("special/formation"); var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); cmpFormation.SetMembers(formation.entities); // If all the selected units were previously in formations of the same shape, // then set this new formation to that shape too; otherwise use the default shape var lastFormationName = undefined; for each (var ent in formation.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { var name = cmpUnitAI.GetLastFormationName(); if (lastFormationName === undefined) { lastFormationName = name; } else if (lastFormationName != name) { lastFormationName = undefined; break; } } } var formationName; if (lastFormationName) formationName = lastFormationName; else formationName = "Line Closed"; if (CanMoveEntsIntoFormation(formation.entities, formationName)) { cmpFormation.LoadFormation(formationName); } else { cmpFormation.LoadFormation("Scatter"); } } return nonformedUnitAIs.concat(Engine.QueryInterface(formationEnt, IID_UnitAI)); } function GetFormationRequirements(formationName) { var countRequired = 1; var classesRequired; switch(formationName) { case "Scatter": case "Column Closed": case "Line Closed": case "Column Open": case "Line Open": case "Battle Line": break; case "Box": countRequired = 4; break; case "Flank": countRequired = 8; break; case "Skirmish": classesRequired = ["Ranged"]; break; case "Wedge": countRequired = 3; classesRequired = ["Cavalry"]; break; case "Phalanx": countRequired = 10; classesRequired = ["Melee", "Infantry"]; break; case "Syntagma": countRequired = 9; classesRequired = ["Melee", "Infantry"]; // TODO: pike only break; case "Testudo": countRequired = 9; classesRequired = ["Melee", "Infantry"]; break; default: // We encountered a unknown formation -> warn the user warn("Commands.js: GetFormationRequirements: unknown formation: " + formationName); return false; } return { "count": countRequired, "classesRequired": classesRequired }; } function CanMoveEntsIntoFormation(ents, formationName) { var count = ents.length; // TODO: should check the player's civ is allowed to use this formation var requirements = GetFormationRequirements(formationName); if (!requirements) return false; if (count < requirements.count) return false; var scatterOnlyUnits = true; for each (var ent in ents) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); if (scatterOnlyUnits && (classes.indexOf("Worker") == -1 || classes.indexOf("Support") == -1)) scatterOnlyUnits = false; for each (var classRequired in requirements.classesRequired) { if (classes.indexOf(classRequired) == -1) { return false; } } } } if (scatterOnlyUnits) return false; return true; } /** * Check if player can control this entity * returns: true if the entity is valid and owned by the player if * or control all units is activated for the player, else false */ function CanControlUnit(entity, player, controlAll) { return (IsOwnedByPlayer(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);} ); } Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements); Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("ProcessCommand", ProcessCommand);