Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 13039) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 13040) @@ -1,1971 +1,1971 @@ 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; const INPUT_NORMAL = 0; const INPUT_SELECTING = 1; const INPUT_BANDBOXING = 2; const INPUT_BUILDING_PLACEMENT = 3; const INPUT_BUILDING_CLICK = 4; const INPUT_BUILDING_DRAG = 5; const INPUT_BATCHTRAINING = 6; const INPUT_PRESELECTEDACTION = 7; const INPUT_BUILDING_WALL_CLICK = 8; const INPUT_BUILDING_WALL_PATHING = 9; var inputState = INPUT_NORMAL; var placementSupport = new PlacementSupport(); 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; var wallDragTooltip = getGUIObjectByName("wallDragTooltip"); if (placementSupport.wallDragTooltip) { wallDragTooltip.caption = placementSupport.wallDragTooltip; wallDragTooltip.hidden = false; } else { wallDragTooltip.caption = ""; wallDragTooltip.hidden = true; } } function updateBuildingPlacementPreview() { // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or // in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to. // See onSimulationUpdate in session.js. if (placementSupport.mode === "building") { if (placementSupport.template && placementSupport.position) { return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, }); } } else if (placementSupport.mode === "wall") { if (placementSupport.wallSet && placementSupport.position) { // Fetch an updated list of snapping candidate entities placementSupport.wallSnapEntities = Engine.PickSimilarFriendlyEntities( placementSupport.wallSet.templates.tower, placementSupport.wallSnapEntitiesIncludeOffscreen, true, // require exact template match true // include foundations ); return Engine.GuiInterfaceCall("SetWallPlacementPreview", { "wallSet": placementSupport.wallSet, "start": placementSupport.position, "end": placementSupport.wallEndPosition, "snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment }); } } return false; } 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); // 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 = ""; var tooltip; // default to walking there var data = {command: "walk"}; if (targetState.garrisonHolder && playerOwned) { data.command = "garrison"; data.target = target; cursor = "action-garrison"; } 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 (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"; } else if (hasClass(entState, "Market") && hasClass(targetState, "Market") && entState.id != targetState.id && (!hasClass(entState, "NavalMarket") || hasClass(targetState, "NavalMarket"))) { // 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 !== null) { data.command = "trade"; data.target = traderData.secondMarket; data.source = traderData.firstMarket; cursor = "action-setup-trade-route"; tooltip = "Click to establish a default route for new traders. Gain: " + gain + " metal."; } } // Don't allow the rally point to be set on any of the currently selected entities for (var i = 0; i < selection.length; i++) if (target === selection[i]) return {"possible": false}; return {"possible": true, "data": data, "position": targetState.position, "cursor": cursor, "tooltip": tooltip}; } 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 neutralOwned = playerState.isNeutral[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 = "Origin trade market."; if (tradingDetails.hasBothMarkets) tooltip += " Gain: " + tradingDetails.gain + " " + tradingDetails.goods + ". Right-click to create a new trade route." else tooltip += " Right-click on another market to set it as a destination trade market." break; case "is second": tooltip = "Destination trade market. Gain: " + tradingDetails.gain + " " + tradingDetails.goods + "." + " Right-click to create a new trade route."; break; case "set first": tooltip = "Set as origin trade market"; break; case "set second": tooltip = "Set as destination trade market. Gain: " + tradingDetails.gain + " " + tradingDetails.goods + "."; break; } return {"possible": true, "tooltip": tooltip}; } break; case "heal": // The check if the target is unhealable is done by targetState.needsHeal if (entState.Healer && hasClass(targetState, "Unit") && targetState.needsHeal && (playerOwned || allyOwned)) { // Healers can't heal themselves. if (entState.id == targetState.id) return {"possible": false}; var unhealableClasses = entState.Healer.unhealableClasses; for each (var unitClass in targetState.identity.classes) { if (unhealableClasses.indexOf(unitClass) != -1) { return {"possible": false}; } } var healableClasses = entState.Healer.healableClasses; for each (var unitClass in targetState.identity.classes) { if (healableClasses.indexOf(unitClass) != -1) { return {"possible": true}; } } } break; case "gather": if (targetState.resourceSupply) { 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 || neutralOwned)) return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": target})}; 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.attack") && getActionInfo("attack", target).possible) { return {"type": "attack", "cursor": "action-attack", "target": target}; } 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, "tooltip": actionInfo.tooltip, "position": actionInfo.position}; else if (getActionInfo("heal", target).possible) return {"type": "heal", "cursor": "action-heal", "target": target}; 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) { if (placementSupport.mode !== "building") { error("[tryPlaceBuilding] Called while in '"+placementSupport.mode+"' placement mode instead of 'building'"); return false; } 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": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); if (!queued) placementSupport.Reset(); return true; } function tryPlaceWall(queued) { if (placementSupport.mode !== "wall") { error("[tryPlaceWall] Called while in '" + placementSupport.mode + "' placement mode; expected 'wall' mode"); return false; } var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object")) { error("[tryPlaceWall] Unexpected return value from updateBuildingPlacementPreview: '" + uneval(placementInfo) + "'; expected either 'false' or 'object'"); return false; } if (!wallPlacementInfo) return false; var selection = g_Selection.toList(); var cmd = { "type": "construct-wall", "autorepair": true, "autocontinue": true, "queued": queued, "entities": selection, "wallSet": placementSupport.wallSet, "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, }; // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed // (this is somewhat non-ideal and hardcode-ish) var hasWallSegment = false; for (var k in cmd.pieces) { if (cmd.pieces[k].template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( { hasWallSegment = true; break; } } if (hasWallSegment) { Engine.PostNetworkCommand(cmd); Engine.GuiInterfaceCall("PlaySound", {"name": "order_repair", "entity": selection[0] }); } 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); // Close the menu when interacting with the game world if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) closeMenu(); // 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 placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_CLICK: // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. switch (ev.type) { case "mousebuttonup": if (ev.button === SDL_BUTTON_LEFT) { inputState = INPUT_BUILDING_WALL_PATHING; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_PATHING: // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint. // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the // user to continue building walls. switch (ev.type) { case "mousemotion": placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!) // the ending point and the starting point to snap to. // // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers // in them. Might be useful to query only for entities within a certain range around the starting point and ending // points. placementSupport.wallSnapEntitiesIncludeOffscreen = true; var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates if (result && result.cost) { placementSupport.wallDragTooltip = getEntityCostTooltip(result); var neededResources = Engine.GuiInterfaceCall("GetNeededResources", result.cost); if (neededResources) placementSupport.wallDragTooltip += getNeededResourcesTooltip(neededResources); } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceWall(queued)) { if (queued) { // continue building, just set a new starting position where we left off placementSupport.position = placementSupport.wallEndPosition; placementSupport.wallEndPosition = undefined; inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.Reset(); inputState = INPUT_NORMAL; } } else { placementSupport.wallDragTooltip = "Cannot build wall here!"; } updateBuildingPlacementPreview(); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // reset to normal input mode placementSupport.Reset(); updateBuildingPlacementPreview(); 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.GetTerrainAtScreenPoint(ev.x, ev.y); placementSupport.angle = Math.atan2(target.x - placementSupport.position.x, target.z - placementSupport.position.z); } else { // If the mouse is near the center, snap back to the default orientation placementSupport.SetDefaultAngle(); } var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.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 placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BATCHTRAINING: switch (ev.type) { case "hotkeyup": if (ev.hotkey == "session.batchtrain") { flushTrainingBatch(); 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(); } if (ev.hotkey == "session.showstatusbars") { g_ShowAllStatusBars = (ev.type == "hotkeydown"); recalculateStatusBarDisplay(); } // 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) { if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) { 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, false); } 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": placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (placementSupport.mode === "wall") { // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities // itself happens in the call to updateBuildingPlacementPreview below). placementSupport.wallSnapEntitiesIncludeOffscreen = false; } else { var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } updateBuildingPlacementPreview(); // includes an update of the snap entity candidates return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { if (placementSupport.mode === "wall") { var validPlacement = updateBuildingPlacementPreview(); if (validPlacement !== false) { inputState = INPUT_BUILDING_WALL_CLICK; } } else { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); dragStart = [ ev.x, ev.y ]; inputState = INPUT_BUILDING_CLICK; } return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); 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": placementSupport.angle += rotation_step; updateBuildingPlacementPreview(); break; case "session.rotate.ccw": placementSupport.angle -= 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.GetTerrainAtScreenPoint(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 "heal": Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "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}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] }); return true; case "garrison": Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] }); 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.GetTerrainAtScreenPoint(ev.x, ev.y); } Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": pos.x, "z": pos.z, "data": action.data, "queued": queued}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": pos.x, "z": pos.z, "queued": queued }); return true; case "unset-rallypoint": var target = Engine.GetTerrainAtScreenPoint(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 // @param buildTemplate Template name of the entity the user wants to build function startBuildingPlacement(buildTemplate) { // TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI // to start building a structure, then the highlight selection rings are kept during the construction of the building. // Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing. placementSupport.Reset(); // find out if we're building a wall, and change the entity appropriately if so var templateData = GetTemplateData(buildTemplate); if (templateData.wallSet) { placementSupport.mode = "wall"; placementSupport.wallSet = templateData.wallSet; inputState = INPUT_BUILDING_PLACEMENT; } else { placementSupport.mode = "building"; placementSupport.template = buildTemplate; inputState = INPUT_BUILDING_PLACEMENT; } } // Called by GUI when user changes preferred trading goods function selectTradingPreferredGoods(data) { 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 batchTrainingEntities; var batchTrainingType; var batchTrainingCount; var batchTrainingEntityAllowedCount; const batchIncrementSize = 5; function flushTrainingBatch() { var appropriateBuildings = getBuildingsWhichCanTrainEntity(batchTrainingEntities, batchTrainingType); // If training limits don't allow us to train batchTrainingCount in each appropriate building if (batchTrainingEntityAllowedCount !== undefined && batchTrainingEntityAllowedCount < batchTrainingCount * appropriateBuildings.length) { // Train as many full batches as we can var buildingsCountToTrainFullBatch = Math.floor(batchTrainingEntityAllowedCount / batchTrainingCount); var buildingsToTrainFullBatch = appropriateBuildings.slice(0, buildingsCountToTrainFullBatch); Engine.PostNetworkCommand({"type": "train", "entities": buildingsToTrainFullBatch, "template": batchTrainingType, "count": batchTrainingCount}); // Train remainer in one more building var remainderToTrain = batchTrainingEntityAllowedCount % batchTrainingCount; Engine.PostNetworkCommand({"type": "train", "entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ], "template": batchTrainingType, "count": remainderToTrain}); } else { Engine.PostNetworkCommand({"type": "train", "entities": appropriateBuildings, "template": batchTrainingType, "count": batchTrainingCount}); } } function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType) { return entitiesToCheck.filter(function(entity) { var state = GetEntityState(entity); var canTrain = state && state.production && state.production.entities.length && state.production.entities.indexOf(trainEntType) != -1; return canTrain; }); } function getEntityLimitAndCount(playerState, entType) { var template = GetTemplateData(entType); var trainingCategory = null; if (template.trainingRestrictions) trainingCategory = template.trainingRestrictions.category; var trainEntLimit = undefined; var trainEntCount = undefined; var canBeTrainedCount = undefined; if (trainingCategory && playerState.entityLimits[trainingCategory]) { trainEntLimit = playerState.entityLimits[trainingCategory]; trainEntCount = playerState.entityCounts[trainingCategory]; canBeTrainedCount = trainEntLimit - trainEntCount; } return [trainEntLimit, trainEntCount, canBeTrainedCount]; } // Add the unit shown at position to the training queue for all entities in the selection function addTrainingByPosition(position) { var simState = Engine.GuiInterfaceCall("GetSimulationState"); var playerState = simState.players[Engine.GetPlayerID()]; var selection = g_Selection.toList(); if (!selection.length) return; var trainableEnts = getAllTrainableEntities(selection); // Check if the position is valid if (!trainableEnts.length || trainableEnts.length <= position) return; var entToTrain = trainableEnts[position]; addTrainingToQueue(selection, entToTrain, playerState); return; } // Called by GUI when user clicks training button function addTrainingToQueue(selection, trainEntType, playerState) { // Create list of buildings which can train trainEntType var appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); // Check trainEntType entity limit and count var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType) // Batch training possible if we can train at least 2 units var batchTrainingPossible = canBeTrainedCount == undefined || canBeTrainedCount > 1; if (Engine.HotkeyIsPressed("session.batchtrain") && batchTrainingPossible) { if (inputState == INPUT_BATCHTRAINING) { // Check if we are training in the same building(s) as the last batch var sameEnts = false; if (batchTrainingEntities.length == selection.length) { // NOTE: We just check if the arrays are the same and if the order is the same // If the order changed, we have a new selection and we should create a new batch. for (var i = 0; i < batchTrainingEntities.length; ++i) { if (!(sameEnts = batchTrainingEntities[i] == selection[i])) break; } } // If we're already creating a batch of this unit (in the same building(s)), then just extend it // (if training limits allow) if (sameEnts && batchTrainingType == trainEntType) { if (canBeTrainedCount == undefined || canBeTrainedCount > batchTrainingCount * appropriateBuildings.length) batchTrainingCount += batchIncrementSize; batchTrainingEntityAllowedCount = canBeTrainedCount; return; } // Otherwise start a new one else { flushTrainingBatch(); // fall through to create the new batch } } inputState = INPUT_BATCHTRAINING; batchTrainingEntities = selection; batchTrainingType = trainEntType; batchTrainingEntityAllowedCount = canBeTrainedCount; batchTrainingCount = batchIncrementSize; } else { // Non-batched - just create a single entity in each building // (but no more than entity limit allows) var buildingsForTraining = appropriateBuildings; if (trainEntLimit) buildingsForTraining = buildingsForTraining.slice(0, canBeTrainedCount); Engine.PostNetworkCommand({"type": "train", "template": trainEntType, "count": 1, "entities": buildingsForTraining}); } } // Called by GUI when user clicks research button function addResearchToQueue(entity, researchType) { Engine.PostNetworkCommand({"type": "research", "entity": entity, "template": researchType}); } // Returns the number of units that will be present in a batch if the user clicks // the training button with shift down function getTrainingBatchStatus(playerState, entity, trainEntType, selection) { var appropriateBuildings = [entity]; if (selection && selection.indexOf(entity) != -1) appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); var nextBatchTrainingCount = 0; if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 && batchTrainingType == trainEntType) { nextBatchTrainingCount = batchTrainingCount; var canBeTrainedCount = batchTrainingEntityAllowedCount; } else { var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType); var batchSize = Math.min(canBeTrainedCount, batchIncrementSize); } // We need to calculate count after the next increment if it's possible if (canBeTrainedCount == undefined || canBeTrainedCount > nextBatchTrainingCount * appropriateBuildings.length) nextBatchTrainingCount += batchIncrementSize; // If training limits don't allow us to train batchTrainingCount in each appropriate building // train as many full batches as we can and remainer in one more building. var buildingsCountToTrainFullBatch = appropriateBuildings.length; var remainderToTrain = 0; if (canBeTrainedCount !== undefined && canBeTrainedCount < nextBatchTrainingCount * appropriateBuildings.length) { buildingsCountToTrainFullBatch = Math.floor(canBeTrainedCount / nextBatchTrainingCount); remainderToTrain = canBeTrainedCount % nextBatchTrainingCount; } return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; } // Called by GUI when user clicks production queue item function removeFromProductionQueue(entity, id) { Engine.PostNetworkCommand({"type": "stop-production", "entity": entity, "id": id}); } // Called by unit selection buttons -function changePrimarySelectionGroup(templateName) +function changePrimarySelectionGroup(templateName, deselectGroup) { - if (Engine.HotkeyIsPressed("session.deselectgroup")) + if (Engine.HotkeyIsPressed("session.deselectgroup") || 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) if (!entState.resourceSupply || !entState.resourceSupply.killBeforeGather) openDeleteDialog(selection); break; case "stop": var selection = g_Selection.toList(); if (selection.length > 0) stopUnits(selection); break; case "garrison": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; break; case "repair": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; break; case "unload-all": unloadAll(); 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": case "add": var toSelect = []; g_Groups.update(); for (var ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); if (action != "add") g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) Engine.CameraFollow(toSelect[0]); 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 }); } } // Lock / Unlock the gate function lockGate(lock) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "lock-gate", "entities": selection, "lock": lock, }); } // Pack / unpack unit(s) function packUnit(pack) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "pack", "entities": selection, "pack": pack, "queued": false }); } // Cancel un/packing unit(s) function cancelPackUnit(pack) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "cancel-pack", "entities": selection, "pack": pack, "queued": false }); } // Transform a wall to a gate function transformWallToGate(template) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "wall-to-gate", "entities": selection.filter( function(e) { return getWallGateTemplate(e) == template } ), "template": template, }); } // Gets the gate form (if any) of a given long wall piece function getWallGateTemplate(entity) { // TODO: find the gate template name in a better way var entState = GetEntityState(entity); var index; if (entState && !entState.foundation && hasClass(entState, "LongWall") && (index = entState.template.indexOf("long")) >= 0) return entState.template.substr(0, index) + "gate"; return undefined; } // 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; var lastIdleType = undefined; function resetIdleUnit() { lastIdleUnit = 0; currIdleClass = 0; lastIdleType = undefined; } function findIdleUnit(classes) { var append = Engine.HotkeyIsPressed("selection.add"); var selectall = Engine.HotkeyIsPressed("selection.offscreen"); // Reset the last idle unit, etc., if the selection type has changed. var type = classes.join(); if (selectall || type != lastIdleType) resetIdleUnit(); lastIdleType = type; // If selectall is true, there is no limit and it's necessary to iterate // over all of the classes, resetting only when the first match is found. var matched = false; for (var i = 0; i < classes.length; ++i) { var data = { idleClass: classes[currIdleClass], prevUnit: lastIdleUnit, limit: 1 }; if (append) data.excludeUnits = g_Selection.toList(); if (selectall) data = { idleClass: classes[currIdleClass] }; // Check if we have new valid entity var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); if (idleUnits.length && idleUnits[0] != lastIdleUnit) { lastIdleUnit = idleUnits[0]; if (!append && (!selectall || selectall && !matched)) g_Selection.reset() if (selectall) g_Selection.addList(idleUnits); else { g_Selection.addList([lastIdleUnit]); Engine.CameraFollow(lastIdleUnit); return; } matched = true; } 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 stopUnits(entities) { Engine.PostNetworkCommand({ "type": "stop", "entities": entities, "queued": false }); } 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 unloadTemplate(template) { // Filter out all entities that aren't garrisonable. var garrisonHolders = g_Selection.toList().filter(function(e) { var state = GetEntityState(e); if (state && state.garrisonHolder) return true; return false; }); Engine.PostNetworkCommand({ "type": "unload-template", "all": Engine.HotkeyIsPressed("session.unloadtype"), "template": template, "garrisonHolders": garrisonHolders }); } function unloadAll() { // Filter out all entities that aren't garrisonable. var garrisonHolders = g_Selection.toList().filter(function(e) { var state = GetEntityState(e); if (state && state.garrisonHolder) return true; return false; }); Engine.PostNetworkCommand({"type": "unload-all", "garrisonHolders": garrisonHolders}); } Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 13039) +++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 13040) @@ -1,1212 +1,1218 @@ // Panel types const SELECTION = "Selection"; const QUEUE = "Queue"; const GARRISON = "Garrison"; const FORMATION = "Formation"; const TRAINING = "Training"; const RESEARCH = "Research"; const CONSTRUCTION = "Construction"; const TRADING = "Trading"; const COMMAND = "Command"; const STANCE = "Stance"; const GATE = "Gate"; const PACK = "Pack"; // 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"]; // Gate constants const GATE_ACTIONS = ["Lock", "Unlock"]; // The number of currently visible buttons (used to optimise showing/hiding) var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Research": 0, "Barter": 0, "Trading": 0, "Construction": 0, "Command": 0, "Stance": 0, "Gate": 0, "Pack": 0}; // Unit panels are panels with row(s) of buttons var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Training", "Barter", "Trading", "Construction", "Research", "Stance", "Command", "Gate", "Pack"]; // Indexes of resources to sell and buy on barter panel var g_barterSell = 0; // 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, buttonSideWidth, buttonSpacer, startIndex, endIndex) { layoutRow("Button", rowNumber, guiName, buttonSideWidth, buttonSpacer, buttonSideWidth, buttonSpacer, startIndex, endIndex); } // Lay out rows function layoutRow(objectName, rowNumber, guiName, objectSideWidth, objectSpacerWidth, objectSideHeight, objectSpacerHeight, startIndex, endIndex) { var colNumber = 0; for (var i = startIndex; i < endIndex; i++) { var button = getGUIObjectByName("unit"+guiName+objectName+"["+i+"]"); if (button) { var size = button.size; size.left = objectSpacerWidth*colNumber; size.right = objectSpacerWidth*colNumber + objectSideWidth; size.top = objectSpacerHeight*rowNumber; size.bottom = objectSpacerHeight*rowNumber + objectSideHeight; button.size = size; colNumber++; } } } // Set the visibility of the object function setOverlay(object, value) { object.hidden = !value; } /** * Format entity count/limit message for the tooltip */ function formatLimitString(trainEntLimit, trainEntCount) { if (trainEntLimit == undefined) return ""; var text = "\n\nCurrent count: " + trainEntCount + ", limit: " + trainEntLimit + "."; if (trainEntCount >= trainEntLimit) text = "[color=\"red\"]" + text + "[/color]"; return text; } /** * Format batch training string for the tooltip * Examples: * buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0: * "Shift-click to train 5" * buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0: * "Shift-click to train 10 (2*5)" * buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12: * "Shift-click to train 27 (15 + 12)" */ function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) { var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; // Don't show the batch training tooltip if either units of this type can't be trained at all // or only one unit can be trained if (totalBatchTrainingCount < 2) return ""; var batchTrainingString = ""; var fullBatchesString = ""; if (buildingsCountToTrainFullBatch > 0) { if (buildingsCountToTrainFullBatch > 1) fullBatchesString += buildingsCountToTrainFullBatch + "*"; fullBatchesString += fullBatchSize; } var remainderBatchString = remainderBatch > 0 ? remainderBatch : ""; var batchDetailsString = ""; // We need to display the batch details part if there is either more than // one building with full batch or one building with the full batch and // another with a partial batch if (buildingsCountToTrainFullBatch > 1 || (buildingsCountToTrainFullBatch == 1 && remainderBatch > 0)) { batchDetailsString += " (" + fullBatchesString; if (remainderBatchString != "") batchDetailsString += " + " + remainderBatchString; batchDetailsString += ")"; } return "\n\n[font=\"serif-bold-13\"]Shift-click[/font][font=\"serif-13\"] to train " + totalBatchTrainingCount + batchDetailsString + ".[/font]"; } /** * Helper function for updateUnitCommands; sets up "unit panels" (i.e. panels with rows of icons) for the currently selected * unit. * * @param guiName Short identifier string of this panel; see constants defined at the top of this file. * @param usedPanels Output object; usedPanels[guiName] will be set to 1 to indicate that this panel was used during this * run of updateUnitCommands and should not be hidden. TODO: why is this done this way instead of having * updateUnitCommands keep track of this? * @param unitEntState Entity state of the (first) selected unit. * @param items Panel-specific data to construct the icons with. * @param callback Callback function to argument to execute when an item's icon gets clicked. Takes a single 'item' argument. */ function setupUnitPanel(guiName, usedPanels, unitEntState, playerState, 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 > 12) numberOfItems = 12; break; case STANCE: if (numberOfItems > 5) numberOfItems = 5; break; case FORMATION: if (numberOfItems > 16) numberOfItems = 16; break; case TRAINING: if (numberOfItems > 24) numberOfItems = 24; break; case RESEARCH: if (numberOfItems > 8) numberOfItems = 8; break; case CONSTRUCTION: if (numberOfItems > 24) numberOfItems = 24; break; case COMMAND: if (numberOfItems > 6) numberOfItems = 6; break; case GATE: if(numberOfItems > 8) numberOfItems = 8; break; case PACK: if(numberOfItems > 8) numberOfItems = 8; break; default: break; } switch (guiName) { case GARRISON: case COMMAND: // Common code for garrison and 'unload all' button counts. for (var i = 0; i < selection.length; ++i) { var state = GetEntityState(selection[i]); if (state.garrisonHolder) garrisonGroups.add(state.garrisonHolder.entities) } break; default: break; } var rowLength = 8; if (guiName == SELECTION) rowLength = 4; else if (guiName == FORMATION || guiName == GARRISON || guiName == COMMAND) rowLength = 4; // Make buttons var i; for (i = 0; i < numberOfItems; i++) { var item = items[i]; // If a tech has been researched it leaves an empty slot if (guiName == RESEARCH && !item) { getGUIObjectByName("unit"+guiName+"Button["+i+"]").hidden = true; // We also remove the paired tech and the pair symbol getGUIObjectByName("unit"+guiName+"Button["+(i+rowLength)+"]").hidden = true; getGUIObjectByName("unit"+guiName+"Pair["+i+"]").hidden = true; continue; } // Get the entity type and load the template for that type if necessary var entType; var template; var entType1; var template1; switch (guiName) { case QUEUE: // The queue can hold both technologies and units so we need to use the correct code for // loading the templates if (item.unitTemplate) { entType = item.unitTemplate; template = GetTemplateData(entType); } else if (item.technologyTemplate) { entType = item.technologyTemplate; template = GetTechnologyData(entType); } if (!template) continue; // ignore attempts to use invalid templates (an error should have been // reported already) break; case RESEARCH: if (item.pair) { entType1 = item.top; template1 = GetTechnologyData(entType1); if (!template1) continue; // ignore attempts to use invalid templates (an error should have been // reported already) entType = item.bottom; } else { entType = item; } template = GetTechnologyData(entType); if (!template) continue; // ignore attempts to use invalid templates (an error should have been // reported already) break; case SELECTION: case GARRISON: case TRAINING: case CONSTRUCTION: entType = item; template = GetTemplateData(entType); if (!template) continue; // ignore attempts to use invalid templates (an error should have been // reported already) break; } switch (guiName) { case SELECTION: var name = getEntityNames(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 = getEntityNames(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; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(item.progress * (size.right - size.left)); getGUIObjectByName("unit"+guiName+"ProgressSlider["+i+"]").size = size; } break; case GARRISON: var name = getEntityNames(template); var tooltip = "Unload " + name + "\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 GATE: var tooltip = item.tooltip; if (item.template) { var template = GetTemplateData(item.template); tooltip += "\n" + getEntityCostTooltip(template); var affordableMask = getGUIObjectByName("unitGateUnaffordable["+i+"]"); affordableMask.hidden = true; var neededResources = Engine.GuiInterfaceCall("GetNeededResources", template.cost); if (neededResources) { affordableMask.hidden = false; tooltip += getNeededResourcesTooltip(neededResources); } } break; case PACK: var tooltip = item.tooltip; break; case STANCE: case FORMATION: var tooltip = toTitleCase(item); break; case TRAINING: var tooltip = getEntityNamesFormatted(template); if (template.tooltip) tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]"; var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = getTrainingBatchStatus(playerState, unitEntState.id, entType, selection); if (Engine.HotkeyIsPressed("session.batchtrain")) { trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; } tooltip += "\n" + getEntityCostTooltip(template, trainNum, unitEntState.id); 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); var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, entType) tooltip += formatLimitString(trainEntLimit, trainEntCount); tooltip += formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch); var key = g_ConfigDB.system["hotkey.session.queueunit." + (i+1)]; if (key !== undefined) { tooltip += "\n[font=\"serif-bold-13\"]HotKey (" + key + ").[/font]"; } break; case RESEARCH: var tooltip = getEntityNamesFormatted(template); if (template.tooltip) tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(template); if (item.pair) { var tooltip1 = getEntityNamesFormatted(template1); if (template1.tooltip) tooltip1 += "\n[font=\"serif-13\"]" + template1.tooltip + "[/font]"; tooltip1 += "\n" + getEntityCostTooltip(template1); } break; case CONSTRUCTION: var tooltip = getEntityNamesFormatted(template); if (template.tooltip) tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(template); // see utility_functions.js tooltip += getPopulationBonusTooltip(template); // see utility_functions.js 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 = garrisonGroups.getTotalCount(); 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 button1 = getGUIObjectByName("unit"+guiName+"Button["+(i+rowLength)+"]"); var affordableMask = getGUIObjectByName("unit"+guiName+"Unaffordable["+i+"]"); var affordableMask1 = getGUIObjectByName("unit"+guiName+"Unaffordable["+(i+rowLength)+"]"); var icon = getGUIObjectByName("unit"+guiName+"Icon["+i+"]"); var selection = getGUIObjectByName("unit"+guiName+"Selection["+i+"]"); var pair = getGUIObjectByName("unit"+guiName+"Pair["+i+"]"); button.hidden = false; button.tooltip = tooltip; // Button Function (need nested functions to get the closure right) // Items can have a callback element that overrides the normal caller-supplied callback function. button.onpress = (function(e){ return function() { e.callback ? e.callback(e) : callback(e) } })(item); + if(guiName == SELECTION) + { + button.onpressright = (function(e){return function() {callback(e, true) } })(item); + button.onpress = (function(e){ return function() {callback(e, false) } })(item); + } + if (guiName == RESEARCH) { if (item.pair) { button.onpress = (function(e){ return function() { callback(e) } })(item.bottom); var icon1 = getGUIObjectByName("unit"+guiName+"Icon["+(i+rowLength)+"]"); button1.hidden = false; button1.tooltip = tooltip1; button1.onpress = (function(e){ return function() { callback(e) } })(item.top); // We add a red overlay to the paired button (we reuse the selection for that) button1.onmouseenter = (function(e){ return function() { setOverlay(e, true) } })(selection); button1.onmouseleave = (function(e){ return function() { setOverlay(e, false) } })(selection); var selection1 = getGUIObjectByName("unit"+guiName+"Selection["+(i+rowLength)+"]");; button.onmouseenter = (function(e){ return function() { setOverlay(e, true) } })(selection1); button.onmouseleave = (function(e){ return function() { setOverlay(e, false) } })(selection1); pair.hidden = false; } else { // Hide the overlay selection.hidden = true; } } // Get icon image if (guiName == FORMATION) { var formationOk = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", { "ents": g_Selection.toList(), "formationName": item }); var grayscale = ""; button.enabled = formationOk; if (!formationOk) { grayscale = "grayscale:"; // 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."; } } var formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "ents": g_Selection.toList(), "formationName": item }); selection.hidden = !formationSelected; icon.sprite = "stretched:"+grayscale+"session/icons/formations/"+item.replace(/\s+/,'').toLowerCase()+".png"; } else if (guiName == STANCE) { var stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", { "ents": g_Selection.toList(), "stance": item }); selection.hidden = !stanceSelected; icon.sprite = "stretched:session/icons/stances/"+item+".png"; } else if (guiName == COMMAND) { icon.sprite = "stretched:session/icons/" + item.icon; } else if (guiName == GATE) { var gateIcon; // If already a gate, show locking actions if (item.gate) { gateIcon = "icons/lock_" + GATE_ACTIONS[item.locked ? 0 : 1].toLowerCase() + "ed.png"; selection.hidden = item.gate.locked === undefined ? false : item.gate.locked != item.locked; } // otherwise show gate upgrade icon else { template = GetTemplateData(item.template); gateIcon = template.icon ? "portraits/" + template.icon : "icons/gate_closed.png"; selection.hidden = true; } icon.sprite = "stretched:session/" + gateIcon; } else if (guiName == PACK) { if (item.packing) { icon.sprite = "stretched:session/icons/cancel.png"; } else { if (item.packed) icon.sprite = "stretched:session/icons/unpack.png"; else icon.sprite = "stretched:session/icons/pack.png"; } } else if (template.icon) { var grayscale = ""; button.enabled = true; if (guiName != SELECTION && template.requiredTechnology && !Engine.GuiInterfaceCall("IsTechnologyResearched", template.requiredTechnology)) { button.enabled = false; var techName = getEntityNames(GetTechnologyData(template.requiredTechnology)); button.tooltip += "\nRequires " + techName; grayscale = "grayscale:"; } if (guiName == RESEARCH && !Engine.GuiInterfaceCall("CheckTechnologyRequirements", entType)) { button.enabled = false; button.tooltip += "\n" + GetTechnologyData(entType).requirementsTooltip; grayscale = "grayscale:"; } icon.sprite = "stretched:" + grayscale + "session/portraits/" + template.icon; if (guiName == RESEARCH) { // Check resource requirements for first button affordableMask.hidden = true; var neededResources = Engine.GuiInterfaceCall("GetNeededResources", template.cost); if (neededResources) { if (button.enabled !== false) { button.enabled = false; affordableMask.hidden = false; } button.tooltip += getNeededResourcesTooltip(neededResources); } if (item.pair) { grayscale = ""; button1.enabled = true; if (!Engine.GuiInterfaceCall("CheckTechnologyRequirements", entType1)) { button1.enabled = false; button1.tooltip += "\n" + GetTechnologyData(entType1).requirementsTooltip; grayscale = "grayscale:"; } icon1.sprite = "stretched:" + grayscale + "session/portraits/" +template1.icon; // Check resource requirements for second button affordableMask1.hidden = true; neededResources = Engine.GuiInterfaceCall("GetNeededResources", template1.cost); if (neededResources) { if (button1.enabled !== false) { button1.enabled = false; affordableMask1.hidden = false; } button1.tooltip += getNeededResourcesTooltip(neededResources); } } else { pair.hidden = true; button1.hidden = true; affordableMask1.hidden = true; } } else if (guiName == CONSTRUCTION || guiName == TRAINING) { if (guiName == TRAINING) { var trainingCategory = null; if (template.trainingRestrictions) trainingCategory = template.trainingRestrictions.category; if (trainingCategory && playerState.entityLimits[trainingCategory] && playerState.entityCounts[trainingCategory] >= playerState.entityLimits[trainingCategory]) grayscale = "grayscale:"; icon.sprite = "stretched:" + grayscale + "session/portraits/" + template.icon; } affordableMask.hidden = true; var totalCosts = {}; var trainNum = 1; if (Engine.HotkeyIsPressed("session.batchtrain") && guiName == TRAINING) { var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = getTrainingBatchStatus(playerState, unitEntState.id, entType, selection); trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; } // Walls have no cost defined. if (template.cost !== undefined) for (var r in template.cost) totalCosts[r] = Math.floor(template.cost[r] * trainNum); var neededResources = Engine.GuiInterfaceCall("GetNeededResources", totalCosts); if (neededResources) { var totalCost = 0; if (button.enabled !== false) { for each (var resource in neededResources) totalCost += resource; button.enabled = false; affordableMask.hidden = false; var alpha = 75 + totalCost/6; alpha = alpha > 150 ? 150 : alpha; affordableMask.sprite = "colour: 255 0 0 " + (alpha); } button.tooltip += getNeededResourcesTooltip(neededResources); } } } 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 numRows = Math.ceil(numButtons / rowLength); var buttonSideLength = getGUIObjectByName("unit"+guiName+"Button[0]").size.bottom; // We sort pairs upside down, so get the size from the topmost button. if (guiName == RESEARCH) buttonSideLength = getGUIObjectByName("unit"+guiName+"Button["+(rowLength*numRows)+"]").size.bottom; var buttonSpacer = buttonSideLength+1; // Layout buttons if (guiName == COMMAND) { layoutButtonRowCentered(0, guiName, 0, numButtons, COMMANDS_PANEL_WIDTH); } else if (guiName == RESEARCH) { // We support pairs so we need to add a row numRows++; // Layout rows from bottom to top for (var i = 0, j = numRows; i < numRows; i++, j--) { layoutButtonRow(i, guiName, buttonSideLength, buttonSpacer, rowLength*(j-1), rowLength*j); } } else { for (var i = 0; i < numRows; i++) layoutButtonRow(i, guiName, buttonSideLength, buttonSpacer, rowLength*i, rowLength*(i+1) ); } // Layout pair icons if (guiName == RESEARCH) { var pairSize = getGUIObjectByName("unit"+guiName+"Pair[0]").size; var pairSideWidth = pairSize.right; var pairSideHeight = pairSize.bottom; var pairSpacerHeight = pairSideHeight + 1; var pairSpacerWidth = pairSideWidth + 1; layoutRow("Pair", 0, guiName, pairSideWidth, pairSpacerWidth, pairSideHeight, pairSpacerHeight, 0, rowLength); } // 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; // Hide unused pair buttons and symbols if (guiName == RESEARCH) { for (i = numButtons; i < g_unitPanelButtons[guiName]; ++i) { getGUIObjectByName("unit"+guiName+"Button["+(i+rowLength)+"]").hidden = true; getGUIObjectByName("unit"+guiName+"Pair["+i+"]").hidden = true; } } g_unitPanelButtons[guiName] = numButtons; } // Sets up "unit trading panel" - special case for setupUnitPanel function setupUnitTradingPanel(usedPanels, unitEntState, selection) { usedPanels[TRADING] = 1; 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 = { "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 selected = getGUIObjectByName("unitTradingSelection["+i+"]"); selected.hidden = !(resource == preferredGoods); var grayscale = (resource != preferredGoods) ? "grayscale:" : ""; icon.sprite = "stretched:"+grayscale+"session/icons/resources/" + resource + ".png"; } } // Sets up "unit barter panel" - special case for setupUnitPanel function setupUnitBarterPanel(unitEntState, playerState) { // 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 action = BARTER_ACTIONS[j]; if (j == 0) { // Display the selection overlay var selection = getGUIObjectByName("unitBarter" + action + "Selection["+i+"]"); selection.hidden = !(i == g_barterSell); } // We gray out the not selected icons in 'sell' row var grayscale = (j == 0 && i != g_barterSell) ? "grayscale:" : ""; 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; // We don't display a button in 'buy' row if the same resource is selected in 'sell' row if (j == 1 && i == g_barterSell) { button.hidden = true; } else { button.hidden = false; button.tooltip = action + " " + resource; icon.sprite = "stretched:"+grayscale+"session/icons/resources/" + resource + ".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() { g_barterSell = i; } })(i); if (i == g_barterSell) { amount = "-" + amountToSell; var neededRes = {}; neededRes[resource] = amountToSell; var neededResources = Engine.GuiInterfaceCall("GetNeededResources", neededRes); var hidden = neededResources ? false : true; for (var ii = 0; ii < BARTER_RESOURCES.length; ii++) { var affordableMask = getGUIObjectByName("unitBarterBuyUnaffordable["+ii+"]"); affordableMask.hidden = hidden; } } else amount = ""; } else { var exchangeResourcesParameters = { "sell": BARTER_RESOURCES[g_barterSell], "buy": BARTER_RESOURCES[i], "amount": amountToSell }; button.onpress = (function(exchangeResourcesParameters){ return function() { exchangeResources(exchangeResourcesParameters); } })(exchangeResourcesParameters); amount = amountToBuy; } getGUIObjectByName("unitBarter" + action + "Amount["+i+"]").caption = amount; } } } /** * Updates the right hand side "Unit Commands" panel. Runs in the main session loop via updateSelectionDetails(). * Delegates to setupUnitPanel to set up individual subpanels, appropriately activated depending on the selected * unit's state. * * @param entState Entity state of the (first) selected unit. * @param supplementalDetailsPanel Reference to the "supplementalSelectionDetails" GUI Object * @param commandsPanel Reference to the "commandsPanel" GUI Object * @param selection Array of currently selected entity IDs. */ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection) { // 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) { // Get player state to check some constraints // e.g. presence of a hero or build limits var simState = Engine.GuiInterfaceCall("GetSimulationState"); var playerState = simState.players[player]; if (selection.length > 1) setupUnitPanel(SELECTION, usedPanels, entState, playerState, g_Selection.groups.getTemplateNames(), - function (entType) { changePrimarySelectionGroup(entType); } ); + function (entType, rightPressed) { changePrimarySelectionGroup(entType, rightPressed); } ); var commands = getEntityCommandsList(entState); if (commands.length) setupUnitPanel(COMMAND, usedPanels, entState, playerState, commands, function (item) { performCommand(entState.id, item.name); } ); if (entState.garrisonHolder) { var groups = new EntityGroups(); for (var i in selection) { state = GetEntityState(selection[i]); if (state.garrisonHolder) groups.add(state.garrisonHolder.entities) } setupUnitPanel(GARRISON, usedPanels, entState, playerState, groups.getTemplateNames(), function (item) { unloadTemplate(item); } ); } var formations = Engine.GuiInterfaceCall("GetAvailableFormations"); if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && !entState.garrisonHolder && formations.length) { setupUnitPanel(FORMATION, usedPanels, entState, playerState, 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") && stances.length) { setupUnitPanel(STANCE, usedPanels, entState, playerState, stances, function (item) { performStance(entState.id, item); } ); } getGUIObjectByName("unitBarterPanel").hidden = !entState.barterMarket; if (entState.barterMarket) { usedPanels["Barter"] = 1; setupUnitBarterPanel(entState, playerState); } var buildableEnts = getAllBuildableEntities(selection); var trainableEnts = getAllTrainableEntities(selection); // Whether the GUI's right panel has been filled. var rightUsed = true; // The first selected entity's type has priority. if (entState.buildEntities) setupUnitPanel(CONSTRUCTION, usedPanels, entState, playerState, buildableEnts, startBuildingPlacement); else if (entState.production && entState.production.entities) setupUnitPanel(TRAINING, usedPanels, entState, playerState, trainableEnts, function (trainEntType) { addTrainingToQueue(selection, trainEntType, playerState); } ); else if (entState.trader) setupUnitTradingPanel(usedPanels, entState, selection); else if (!entState.foundation && entState.gate || hasClass(entState, "LongWall")) { // Allow long wall pieces to be converted to gates var longWallTypes = {}; var walls = []; var gates = []; for (var i in selection) { state = GetEntityState(selection[i]); if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template]) { var gateTemplate = getWallGateTemplate(state.id); if (gateTemplate) { var wallName = GetTemplateData(state.template).name.generic; var gateName = GetTemplateData(gateTemplate).name.generic; walls.push({ "tooltip": "Convert " + wallName + " to " + gateName, "template": gateTemplate, "callback": function (item) { transformWallToGate(item.template); } }); } // We only need one entity per type. longWallTypes[state.template] = true; } else if (state.gate && !gates.length) for (var j = 0; j < GATE_ACTIONS.length; ++j) gates.push({ "gate": state.gate, "tooltip": GATE_ACTIONS[j] + " gate", "locked": j == 0, "callback": function (item) { lockGate(item.locked); } }); // Show both 'locked' and 'unlocked' as active if the selected gates have both lock states. else if (state.gate && state.gate.locked != gates[0].gate.locked) for (var j = 0; j < gates.length; ++j) delete gates[j].gate.locked; } // Place wall conversion options after gate lock/unlock icons. var items = gates.concat(walls); if (items.length) setupUnitPanel(GATE, usedPanels, entState, playerState, items); else rightUsed = false; } else if (entState.pack) { var items = []; var packButton = false; var unpackButton = false; var packCancelButton = false; var unpackCancelButton = false; for (var i in selection) { // Find un/packable entities var state = GetEntityState(selection[i]); if (state.pack) { if (state.pack.progress == 0) { if (!state.pack.packed) packButton = true; else if (state.pack.packed) unpackButton = true; } else { // Already un/packing - show cancel button if (!state.pack.packed) packCancelButton = true; else if (state.pack.packed) unpackCancelButton = true; } } } if (packButton) items.push({ "packing": false, "packed": false, "tooltip": "Pack", "callback": function() { packUnit(true); } }); if (unpackButton) items.push({ "packing": false, "packed": true, "tooltip": "Unpack", "callback": function() { packUnit(false); } }); if (packCancelButton) items.push({ "packing": true, "packed": false, "tooltip": "Cancel packing", "callback": function() { cancelPackUnit(true); } }); if (unpackCancelButton) items.push({ "packing": true, "packed": true, "tooltip": "Cancel unpacking", "callback": function() { cancelPackUnit(false); } }); if (items.length) setupUnitPanel(PACK, usedPanels, entState, playerState, items); else rightUsed = false; } else rightUsed = false; if (!rightUsed) { // The right pane is empty. Fill the pane with a sane type. // Prefer buildables for units and trainables for structures. if (buildableEnts.length && (hasClass(entState, "Unit") || !trainableEnts.length)) setupUnitPanel(CONSTRUCTION, usedPanels, entState, playerState, buildableEnts, startBuildingPlacement); else if (trainableEnts.length) setupUnitPanel(TRAINING, usedPanels, entState, playerState, trainableEnts, function (trainEntType) { addTrainingToQueue(selection, trainEntType, playerState); } ); } // Show technologies if the active panel has at most one row of icons. if (entState.production && entState.production.technologies.length) { var activepane = usedPanels[CONSTRUCTION] ? buildableEnts.length : trainableEnts.length; if (selection.length == 1 || activepane <= 8) setupUnitPanel(RESEARCH, usedPanels, entState, playerState, entState.production.technologies, function (researchType) { addResearchToQueue(entState.id, researchType); } ); } if (entState.production && entState.production.queue.length) setupUnitPanel(QUEUE, usedPanels, entState, playerState, entState.production.queue, function (item) { removeFromProductionQueue(entState.id, item.id); } ); supplementalDetailsPanel.hidden = false; commandsPanel.hidden = false; } else // owned by another player { 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; } // Get all of the available entities which can be trained by the selected entities function getAllTrainableEntities(selection) { var trainableEnts = []; var state; // Get all buildable and trainable entities for (var i in selection) { if ((state = GetEntityState(selection[i])) && state.production && state.production.entities.length) trainableEnts = trainableEnts.concat(state.production.entities); } // Remove duplicates removeDupes(trainableEnts); return trainableEnts; } // Get all of the available entities which can be built by the selected entities function getAllBuildableEntities(selection) { var buildableEnts = []; var state; // Get all buildable entities for (var i in selection) { if ((state = GetEntityState(selection[i])) && state.buildEntities && state.buildEntities.length) buildableEnts = buildableEnts.concat(state.buildEntities); } // Remove duplicates removeDupes(buildableEnts); return buildableEnts; } Index: ps/trunk/source/gui/GUIbase.h =================================================================== --- ps/trunk/source/gui/GUIbase.h (revision 13039) +++ ps/trunk/source/gui/GUIbase.h (revision 13040) @@ -1,223 +1,225 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* GUI Core, stuff that the whole GUI uses --Overview-- Contains defines, includes, types etc that the whole GUI should have included. --More info-- Check GUI.h */ #ifndef INCLUDED_GUIBASE #define INCLUDED_GUIBASE //-------------------------------------------------------- // Includes / Compiler directives //-------------------------------------------------------- #include #include // I would like to just forward declare CSize, but it doesn't // seem to be defined anywhere in the predefined header. #include "ps/Overlay.h" #include "ps/CStr.h" #include "ps/Errors.h" //-------------------------------------------------------- // Forward declarations //-------------------------------------------------------- class IGUIObject; //-------------------------------------------------------- // Macros //-------------------------------------------------------- // Object settings setups // Setup an object's ConstructObject function #define GUI_OBJECT(obj) \ public: \ static IGUIObject *ConstructObject() { return new obj(); } //-------------------------------------------------------- // Types //-------------------------------------------------------- /** * Message types. * @see SGUIMessage */ enum EGUIMessageType { GUIM_MOUSE_OVER, GUIM_MOUSE_ENTER, GUIM_MOUSE_LEAVE, GUIM_MOUSE_PRESS_LEFT, GUIM_MOUSE_PRESS_RIGHT, GUIM_MOUSE_DOWN_LEFT, GUIM_MOUSE_DOWN_RIGHT, GUIM_MOUSE_DBLCLICK_LEFT, GUIM_MOUSE_DBLCLICK_RIGHT, GUIM_MOUSE_RELEASE_LEFT, GUIM_MOUSE_RELEASE_RIGHT, GUIM_MOUSE_WHEEL_UP, GUIM_MOUSE_WHEEL_DOWN, GUIM_SETTINGS_UPDATED, // SGUIMessage.m_Value = name of setting GUIM_PRESSED, GUIM_DOUBLE_PRESSED, GUIM_MOUSE_MOTION, GUIM_LOAD, // Called when an object is added to the GUI. GUIM_GOT_FOCUS, - GUIM_LOST_FOCUS + GUIM_LOST_FOCUS, + GUIM_PRESSED_MOUSE_RIGHT, + GUIM_DOUBLE_PRESSED_MOUSE_RIGHT }; /** * Message send to IGUIObject::HandleMessage() in order * to give life to Objects manually with * a derived HandleMessage(). */ struct SGUIMessage { SGUIMessage(EGUIMessageType _type) : type(_type), skipped(false) {} SGUIMessage(EGUIMessageType _type, const CStr& _value) : type(_type), value(_value), skipped(false) {} /** * This method can be used to allow other event handlers to process this GUI event, * by default an event is not skipped (only the first handler will process it). * * @param skip true to allow further event handling, false to prevent it */ void Skip(bool skip = true) { skipped = skip; } /** * Describes what the message regards */ EGUIMessageType type; /** * Optional data */ CStr value; /** * Flag that specifies if object skipped handling the event */ bool skipped; }; /** * Recurse restrictions, when we recurse, if an object * is hidden for instance, you might want it to skip * the children also * Notice these are flags! and we don't really need one * for no restrictions, because then you'll just enter 0 */ enum { GUIRR_HIDDEN = 0x00000001, GUIRR_DISABLED = 0x00000010, GUIRR_GHOST = 0x00000100 }; // Text alignments enum EAlign { EAlign_Left, EAlign_Right, EAlign_Center }; enum EVAlign { EVAlign_Top, EVAlign_Bottom, EVAlign_Center }; // Typedefs typedef std::map map_pObjects; typedef std::vector vector_pObjects; // Icon, you create them in the XML file with root element // you use them in text owned by different objects... Such as CText. struct SGUIIcon { SGUIIcon() : m_CellID(0) {} // Sprite name of icon CStr m_SpriteName; // Size CSize m_Size; // Cell of texture to use; ignored unless the texture has specified cell-size int m_CellID; }; /** * Client Area is a rectangle relative to a parent rectangle * * You can input the whole value of the Client Area by * string. Like used in the GUI. */ class CClientArea { public: CClientArea(); CClientArea(const CStr& Value); CClientArea(const CRect& pixel, const CRect& percent); /// Pixel modifiers CRect pixel; /// Percent modifiers CRect percent; /** * Get client area rectangle when the parent is given */ CRect GetClientArea(const CRect &parent) const; /** * The ClientArea can be set from a string looking like: * * "0 0 100% 100%" * "50%-10 50%-10 50%+10 50%+10" * * i.e. First percent modifier, then + or - and the pixel modifier. * Although you can use just the percent or the pixel modifier. Notice * though that the percent modifier must always be the first when * both modifiers are inputted. * * @return true if success, false if failure. If false then the client area * will be unchanged. */ bool SetClientArea(const CStr& Value); }; //-------------------------------------------------------- // Error declarations //-------------------------------------------------------- ERROR_GROUP(GUI); ERROR_TYPE(GUI, NullObjectProvided); ERROR_TYPE(GUI, InvalidSetting); ERROR_TYPE(GUI, OperationNeedsGUIObject); ERROR_TYPE(GUI, NameAmbiguity); ERROR_TYPE(GUI, ObjectNeedsName); #endif Index: ps/trunk/source/gui/IGUIButtonBehavior.cpp =================================================================== --- ps/trunk/source/gui/IGUIButtonBehavior.cpp (revision 13039) +++ ps/trunk/source/gui/IGUIButtonBehavior.cpp (revision 13040) @@ -1,146 +1,190 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* IGUIButtonBehavior */ #include "precompiled.h" #include "GUI.h" //------------------------------------------------------------------- // Constructor / Destructor //------------------------------------------------------------------- IGUIButtonBehavior::IGUIButtonBehavior() : m_Pressed(false) { } IGUIButtonBehavior::~IGUIButtonBehavior() { } void IGUIButtonBehavior::HandleMessage(SGUIMessage &Message) { // TODO Gee: easier access functions switch (Message.type) { case GUIM_MOUSE_PRESS_LEFT: { bool enabled; GUI::GetSetting(this, "enabled", enabled); if (!enabled) break; m_Pressed = true; - } break; + } + break; + + case GUIM_MOUSE_DBLCLICK_RIGHT: + case GUIM_MOUSE_RELEASE_RIGHT: + { + bool enabled; + GUI::GetSetting(this, "enabled", enabled); + + if (!enabled) + break; + + if (m_PressedRight) + { + m_PressedRight = false; + if (Message.type == GUIM_MOUSE_RELEASE_RIGHT) + { + // Button was right-clicked + SendEvent(GUIM_PRESSED_MOUSE_RIGHT, "pressright"); + } + else + { + // Button was clicked a second time. We can't tell if the button + // expects to receive doublepress events or just a second press + // event, so send both of them (and assume the extra unwanted press + // is harmless on buttons that expect doublepress) + SendEvent(GUIM_PRESSED_MOUSE_RIGHT, "pressright"); + SendEvent(GUIM_DOUBLE_PRESSED_MOUSE_RIGHT, "doublepressright"); + } + } + } + break; + + case GUIM_MOUSE_PRESS_RIGHT: + { + bool enabled; + GUI::GetSetting(this, "enabled", enabled); + + if (!enabled) + break; + + m_PressedRight = true; + } + break; case GUIM_MOUSE_DBLCLICK_LEFT: case GUIM_MOUSE_RELEASE_LEFT: { bool enabled; GUI::GetSetting(this, "enabled", enabled); if (!enabled) break; if (m_Pressed) { m_Pressed = false; if (Message.type == GUIM_MOUSE_RELEASE_LEFT) { // Button was clicked SendEvent(GUIM_PRESSED, "press"); } else { // Button was clicked a second time. We can't tell if the button // expects to receive doublepress events or just a second press // event, so send both of them (and assume the extra unwanted press // is harmless on buttons that expect doublepress) SendEvent(GUIM_PRESSED, "press"); SendEvent(GUIM_DOUBLE_PRESSED, "doublepress"); } } - } break; + } + break; default: break; } } CColor IGUIButtonBehavior::ChooseColor() { CColor color, color2; // Yes, the object must possess these settings. They are standard GUI::GetSetting(this, "textcolor", color); bool enabled; GUI::GetSetting(this, "enabled", enabled); if (!enabled) { GUI::GetSetting(this, "textcolor_disabled", color2); return GUI<>::FallBackColor(color2, color); } else if (m_MouseHovering) { if (m_Pressed) { GUI::GetSetting(this, "textcolor_pressed", color2); return GUI<>::FallBackColor(color2, color); } else { GUI::GetSetting(this, "textcolor_over", color2); return GUI<>::FallBackColor(color2, color); } } else return color; } void IGUIButtonBehavior::DrawButton(const CRect &rect, const float &z, CGUISpriteInstance& sprite, CGUISpriteInstance& sprite_over, CGUISpriteInstance& sprite_pressed, CGUISpriteInstance& sprite_disabled, int cell_id) { if (GetGUI()) { bool enabled; GUI::GetSetting(this, "enabled", enabled); if (!enabled) { GetGUI()->DrawSprite(GUI<>::FallBackSprite(sprite_disabled, sprite), cell_id, z, rect); } else if (m_MouseHovering) { if (m_Pressed) GetGUI()->DrawSprite(GUI<>::FallBackSprite(sprite_pressed, sprite), cell_id, z, rect); else GetGUI()->DrawSprite(GUI<>::FallBackSprite(sprite_over, sprite), cell_id, z, rect); } else GetGUI()->DrawSprite(sprite, cell_id, z, rect); } } Index: ps/trunk/source/gui/IGUIButtonBehavior.h =================================================================== --- ps/trunk/source/gui/IGUIButtonBehavior.h (revision 13039) +++ ps/trunk/source/gui/IGUIButtonBehavior.h (revision 13040) @@ -1,130 +1,132 @@ /* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* GUI Object Base - Button Behavior --Overview-- Interface class that enhance the IGUIObject with buttony behavior (click and release to click a button), and the GUI message GUIM_PRESSED. When creating a class with extended settings and buttony behavior, just do a multiple inheritance. --More info-- Check GUI.h */ #ifndef INCLUDED_IGUIBUTTONBEHAVIOR #define INCLUDED_IGUIBUTTONBEHAVIOR //-------------------------------------------------------- // Includes / Compiler directives //-------------------------------------------------------- #include "GUI.h" class CGUISpriteInstance; //-------------------------------------------------------- // Macros //-------------------------------------------------------- //-------------------------------------------------------- // Types //-------------------------------------------------------- //-------------------------------------------------------- // Declarations //-------------------------------------------------------- /** * Appends button behaviours to the IGUIObject. * Can be used with multiple inheritance alongside * IGUISettingsObject and such. * * @see IGUIObject */ class IGUIButtonBehavior : virtual public IGUIObject { public: IGUIButtonBehavior(); virtual ~IGUIButtonBehavior(); /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage &Message); /** * This is a function that lets a button being drawn, * it regards if it's over, disabled, pressed and such. * You input sprite names and area and it'll output * it accordingly. * * This class is meant to be used manually in Draw() * * @param rect Rectangle in which the sprite should be drawn * @param z Z-value * @param sprite Sprite drawn when not pressed, hovered or disabled * @param sprite_over Sprite drawn when m_MouseHovering is true * @param sprite_pressed Sprite drawn when m_Pressed is true * @param sprite_disabled Sprite drawn when "enabled" is false * @param cell_id Identifies the icon to be used (if the sprite contains * cell-using images) */ void DrawButton(const CRect &rect, const float &z, CGUISpriteInstance& sprite, CGUISpriteInstance& sprite_over, CGUISpriteInstance& sprite_pressed, CGUISpriteInstance& sprite_disabled, int cell_id); /** * Choosing which color of the following according to object enabled/hovered/pressed status: * textcolor_disabled -- disabled * textcolor_pressed -- pressed * textcolor_over -- hovered */ CColor ChooseColor(); protected: /** * @see IGUIObject#ResetStates() */ virtual void ResetStates() { // Notify the gui that we aren't hovered anymore UpdateMouseOver(NULL); m_Pressed = false; + m_PressedRight = false; } /** * Everybody knows how a button works, you don't simply press it, * you have to first press the button, and then release it... * in between those two steps you can actually leave the button * area, as long as you release it within the button area... Anyway * this lets us know we are done with step one (clicking). */ bool m_Pressed; + bool m_PressedRight; }; #endif