Index: ps/trunk/binaries/data/mods/public/globalscripts/l10n.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/l10n.js (revision 15374) +++ ps/trunk/binaries/data/mods/public/globalscripts/l10n.js (revision 15375) @@ -1,213 +1,218 @@ var g_translations = {}; var g_pluralTranslations = {}; var g_translationsWithContext = {}; var g_pluralTranslationsWithContext = {}; // Checks if the specified variable is a string, and if it is, it checks that it // is not empty. function isNonEmptyString(variable) { if (typeof variable != "string") return false; else if (variable.trim()) return true; else return false; } // Translates the specified English message into the current language. // // This function relies on the g_translations cache when possible. You should use this function instead of // Engine.Translate() whenever you can to minimize the number of C++ calls and string conversions involved. function translate(message) { var translation = g_translations[message]; if (!translation) return g_translations[message] = Engine.Translate(message); return translation; } // Translates the specified English message into the current language for the specified number. // // This function relies on the g_pluralTranslations cache when possible. You should use this function instead of // Engine.TranslatePlural() whenever you can to minimize the number of C++ calls and string conversions involved. function translatePlural(singularMessage, pluralMessage, number) { var translation = g_pluralTranslations[singularMessage]; if (!translation) g_pluralTranslations[singularMessage] = {}; var pluralTranslation = g_pluralTranslations[singularMessage][number]; if (!pluralTranslation) return g_pluralTranslations[singularMessage][number] = Engine.TranslatePlural(singularMessage, pluralMessage, number); return pluralTranslation; } // Translates the specified English message into the current language for the specified context. // // This function relies on the g_translationsWithContext cache when possible. You should use this function instead of // Engine.TranslateWithContext() whenever you can to minimize the number of C++ calls and string conversions involved. function translateWithContext(context, message) { var translationContext = g_translationsWithContext[context]; if (!translationContext) g_translationsWithContext[context] = {} var translationWithContext = g_translationsWithContext[context][message]; if (!translationWithContext) return g_translationsWithContext[context][message] = Engine.TranslateWithContext(context, message); return translationWithContext; } // Translates the specified English message into the current language for the specified context and number. // // This function relies on the g_pluralTranslationsWithContext cache when possible. You should use this function instead of // Engine.TranslatePluralWithContext() whenever you can to minimize the number of C++ calls and string conversions involved. function translatePluralWithContext(context, singularMessage, pluralMessage, number) { var translationContext = g_pluralTranslationsWithContext[context]; if (!translationContext) g_pluralTranslationsWithContext[context] = {}; var translationWithContext = g_pluralTranslationsWithContext[context][singularMessage]; if (!translationWithContext) g_pluralTranslationsWithContext[context][singularMessage] = {}; var pluralTranslationWithContext = g_pluralTranslationsWithContext[context][singularMessage][number]; if (!pluralTranslationWithContext) return g_pluralTranslationsWithContext[context][singularMessage][number] = Engine.TranslatePluralWithContext(context, singularMessage, pluralMessage, number); return pluralTranslationWithContext; } /** * The input object should contain either of the following properties: * * • A ‘message’ property that contains a message to translate. * * • A ‘list’ property that contains a list of messages to translate as a * comma-separated list of translated. * * Optionally, the input object may contain a ‘context’ property. In that case, * the value of this property is used as translation context, that is, passed to * the translateWithContext(context, message) function. */ function translateMessageObject(object) { // the translation function var trans = translate; if (object.context) trans = function(msg) { return translateWithContext(object.context, msg);}; if (object.message) object = trans(object.message); else if (object.list) { var translatedList = object.list.map(trans); object = translatedList.join(translateWithContext("enumeration", ", ")); } return object; } /** * Translates any string value in the specified JavaScript object * that is associated with a key included in the specified keys array. * * it accepts an object in the form of * * { * translatedString1: "my first message", * unTranslatedString1: "some English string", * ignoredObject: { * translatedString2: "my second message", * unTranslatedString2: "some English string" * }, * translatedObject1: { * message: "my third singular message", * context: "message context", * }, * translatedObject2: { * list: ["list", "of", "strings"], * context: "message context", * }, * } * * Together with a keys list to translate the strings and objects * ["translatedString1", "translatedString2", "translatedObject1", * "translatedObject2"] * * The result will be (f.e. in Dutch) * { * translatedString1: "mijn eerste bericht", * unTranslatedString1: "some English string", * ignoredObject: { * translatedString2: "mijn tweede bericht", * unTranslatedString2: "some English string" * }, * translatedObject1: "mijn derde bericht", * translatedObject2: "lijst, van, teksten", * } * * So you see that the keys array can also contain lower-level keys, * And that you can include objects in the keys array to translate * them with a context, or to join a list of translations. * * Also, the keys array may be an object where properties are keys to translate * and values are translation contexts to use for each key. */ function translateObjectKeys(object, keys) { // If ‘keys’ is an array, simply translate. if (keys instanceof Array) { for (var property in object) { if (keys.indexOf(property) > -1) { if (isNonEmptyString(object[property])) object[property] = translate(object[property]); else if (object[property] instanceof Object) { object[property] = translateMessageObject(object[property]); } } else if (object[property] instanceof Object) translateObjectKeys(object[property], keys); } } // If ‘keys’ is not an array, it is an object where keys are properties to // translate and values are translation contexts to use for each key. // An empty value means no context. else { for (var property in object) { if (property in keys) { if (isNonEmptyString(object[property])) if (keys[property]) object[property] = translateWithContext(keys[property], object[property]); else object[property] = translate(object[property]); else if (object[property] instanceof Object) object[property] = translateMessageObject(object[property]); } else if (object[property] instanceof Object) translateObjectKeys(object[property], keys); } } } +/** + * Function is used by the extract-messages tool. + * So it may only be used on a plain string, + * it won't have any effect on a calculated string. + */ function markForTranslation(message) { return message; } function markForTranslationWithContext(context, message) { return message; } Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 15374) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 15375) @@ -1,1847 +1,1849 @@ 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; const ACTION_GUARD = 3; 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; const INPUT_MASSTRIBUTING = 10; var inputState = INPUT_NORMAL; var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; // Distance to search for a selatable entity in. Bigger numbers are slower. var SELECTION_SEARCH_RADIUS = 100; // 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 = Engine.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 placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); if (placementSupport.tooltipMessage) { if (placementSupport.tooltipError) placementTooltip.sprite = "BackgroundErrorTooltip"; else placementTooltip.sprite = "BackgroundInformationTooltip"; placementTooltip.caption = placementSupport.tooltipMessage; placementTooltip.hidden = false; } else { placementTooltip.caption = ""; placementTooltip.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) { var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed }); // Show placement info tooltip if invalid position placementSupport.tooltipError = !result.success; placementSupport.tooltipMessage = ""; if (!result.success) { if (result.message && result.parameters) { var message = result.message; if (result.translateMessage) message = translate(message); var parameters = result.parameters; if (result.translateParameters) translateObjectKeys(parameters, result.translateParameters); placementSupport.tooltipMessage = sprintf(message, parameters); } return false; } if (placementSupport.attack) { // building can be placed here, and has an attack // show the range advantage in the tooltip var cmd = {x: placementSupport.position.x, z: placementSupport.position.z, range: placementSupport.attack.maxRange, elevationBonus: placementSupport.attack.elevationBonus, }; var averageRange = Engine.GuiInterfaceCall("GetAverageRangeForBuildings",cmd); placementSupport.tooltipMessage = sprintf(translate("Basic range: %(range)s"), { range: Math.round(cmd.range/4) }) + "\n" + sprintf(translate("Average bonus range: %(range)s"), { range: Math.round((averageRange - cmd.range)/4) }); } return true; } } 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 (!("resourceGatherRates" in gatherer) || !gatherer.resourceGatherRates || !supply) return undefined; if (gatherer.resourceGatherRates[supply.type.generic+"."+supply.type.specific]) return supply.type.specific; if (gatherer.resourceGatherRates[supply.type.generic]) return supply.type.generic; return undefined; } function getActionInfo(action, target) { var simState = GetSimState(); var selection = g_Selection.toList(); // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return {"possible": false}; if (!target) // TODO move these non-target actions to an object like unit_actions.js { if (action == "set-rallypoint") { var cursor = ""; var data = {command: "walk"}; if (Engine.HotkeyIsPressed("session.attackmove")) { data = {command: "attack-walk"}; cursor = "action-attack-move"; } return {"possible": true, "data": data, "cursor": cursor}; } else if (action == "move" || action == "attack-move") return {"possible": true}; else if (action == "remove-guard") return {"possible": true}; else return {"possible": false}; } // 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 = GetExtendedEntityState(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 for each (var entityID in selection) { var entState = GetExtendedEntityState(entityID); if (!entState) continue; if (unitActions[action] && unitActions[action].getActionInfo) { var r = unitActions[action].getActionInfo(entState, targetState, simState); if (r) // return true if it's possible for one of the entities return r; } } 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; var targets = []; var target = undefined; if (!fromMinimap) targets = Engine.PickEntitiesAtPoint(x, y, SELECTION_SEARCH_RADIUS); if (targets.length) target = targets[0]; // decide between the following ordered actions // if two actions are possible, the first one is taken // so the most specific should appear first var actions = Object.keys(unitActions).slice(); - actions.sort(function(a, b) {return unitActions[a].specificness > unitActions[b].specificness;}); + actions.sort(function(a, b) {return unitActions[a].specificness - unitActions[b].specificness;}); var actionInfo = undefined; if (preSelectedAction != ACTION_NONE) { for (var action of actions) { if (unitActions[action].preSelectedActionCheck) { var r = unitActions[action].preSelectedActionCheck(target, selection); if (r) return r; } } return {"type": "none", "cursor": "", "target": target}; } for (var action of actions) { if (unitActions[action].hotkeyActionCheck) { var r = unitActions[action].hotkeyActionCheck(target, selection); if (r) return r; } } for (var action of actions) { if (unitActions[action].actionCheck) { var r = unitActions[action].actionCheck(target, selection); if (r) return r; } } return {"type": "none", "cursor": "", "target": target}; } var dragStart; // used for remembering mouse coordinates at start of drag operations function tryPlaceBuilding(queued) { if (placementSupport.mode !== "building") { error(sprintf("[%(functionName)s] Called while in '%(mode)s' placement mode instead of 'building'", { functionName: "tryPlaceBuilding", mode: placementSupport.mode })); return false; } // 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; } var selection = g_Selection.toList(); // Start the construction Engine.PostNetworkCommand({ "type": "construct", "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); if (!queued) placementSupport.Reset(); else placementSupport.RandomizeActorSeed(); return true; } function tryPlaceWall(queued) { if (placementSupport.mode !== "wall") { error(sprintf("[%(functionName)s] Called while in '%(mode)s' placement mode; expected 'wall' mode", { functionName: "tryPlaceWall", mode: placementSupport.mode })); return false; } var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object")) { error(sprintf("[%(functionName)s] Unexpected return value from %(function2Name)s: '%(value)s'; expected either 'false' or 'object'", { functionName: "tryPlaceWall", function2Name: "updateBuildingPlacementPreview", value: uneval(placementInfo) })); 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 = Engine.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 = Engine.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 = Engine.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.tooltipMessage = getEntityCostTooltip(result); var neededResources = Engine.GuiInterfaceCall("GetNeededResources", result.cost); if (neededResources) placementSupport.tooltipMessage += 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.tooltipMessage = translate("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_MASSTRIBUTING: if (ev.type == "hotkeyup" && ev.hotkey == "session.masstribute") { flushTributing(); inputState = INPUT_NORMAL; } break; case INPUT_BATCHTRAINING: if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain") { flushTrainingBatch(); inputState = INPUT_NORMAL; } break; } return false; } function handleInputAfterGui(ev) { if (ev.hotkey === undefined) ev.hotkey = null; // Handle the time-warp testing features, restricted to single-player if (!g_IsNetworked && Engine.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(); } if (ev.hotkey == "session.highlightguarding") { g_ShowGuarding = (ev.type == "hotkeydown"); updateAdditionalHighlight(); } if (ev.hotkey == "session.highlightguarded") { g_ShowGuarded = (ev.type == "hotkeydown"); updateAdditionalHighlight(); } // 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, SELECTION_SEARCH_RADIUS); 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 "mousemotion": // Highlight the first hovered entity (if any) var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS); if (ents.length) g_Selection.setHighlightList([ents[0]]); else g_Selection.setHighlightList([]); return false; 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, SELECTION_SEARCH_RADIUS); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS); 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 = GetEntityState(selectedEntity).identity.selectionGroupName; if (templateToMatch) { matchRank = false; } else { // No selection group name defined, so fall back to exact match templateToMatch = 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 = 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"); var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (unitActions[action.type] && unitActions[action.type].execute) return unitActions[action.type].execute(target, action, selection, queued); 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) return false; var fromMinimap = true; var action = determineAction(undefined, undefined, fromMinimap); if (!action) return false; var selection = g_Selection.toList(); var queued = Engine.HotkeyIsPressed("session.queue"); if (unitActions[action.type] && unitActions[action.type].execute) return unitActions[action.type].execute(target, action, selection, queued); 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, playerState) { - if(getEntityLimitAndCount(playerState, buildTemplate)[2] == 0) + if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0) return; // 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; } if (templateData.attack && templateData.attack.Ranged && templateData.attack.Ranged.maxRange) { // add attack information to display a good tooltip placementSupport.attack = templateData.attack.Ranged; } } // Called by GUI when user changes required trading goods function selectRequiredGoods(data) { Engine.PostNetworkCommand({"type": "select-required-goods", "entities": data.entities, "requiredGoods": data.requiredGoods}); } // 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}); } // Camera jumping: when the user presses a hotkey the current camera location is marked. // When they press another hotkey the camera jumps back to that position. If the camera is already roughly at that location, // jump back to where it was previously. var jumpCameraPositions = [], jumpCameraLast; function jumpCamera(index) { var position = jumpCameraPositions[index], distanceThreshold = Engine.ConfigDB_GetValue("user", "camerajump.threshold"); if (position) { if (jumpCameraLast && Math.abs(Engine.CameraGetX() - position.x) < distanceThreshold && Math.abs(Engine.CameraGetZ() - position.z) < distanceThreshold) Engine.CameraMoveTo(jumpCameraLast.x, jumpCameraLast.z); else { jumpCameraLast = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()}; Engine.CameraMoveTo(position.x, position.z); } } } function setJumpCamera(index) { jumpCameraPositions[index] = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()}; } // 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 r = { + "entLimit": undefined, + "entCount": undefined, + "entLimitChangers": undefined, + "canBeAddedCount": undefined + }; var template = GetTemplateData(entType); var entCategory = null; if (template.trainingRestrictions) entCategory = template.trainingRestrictions.category; else if (template.buildRestrictions) entCategory = template.buildRestrictions.category; - var entLimit = undefined; - var entCount = undefined; - var entLimitChangers = undefined; - var canBeAddedCount = undefined; if (entCategory && playerState.entityLimits[entCategory] != null) { - entLimit = playerState.entityLimits[entCategory]; - entCount = playerState.entityCounts[entCategory]; - entLimitChangers = playerState.entityLimitChangers[entCategory]; - canBeAddedCount = Math.max(entLimit - entCount, 0); + r.entLimit = playerState.entityLimits[entCategory] || Infinity; + r.entCount = playerState.entityCounts[entCategory] || 0; + r.entLimitChangers = playerState.entityLimitChangers[entCategory]; + r.canBeAddedCount = Math.max(r.entLimit - r.entCount, 0); } - return [entLimit, entCount, canBeAddedCount, entLimitChangers]; + return r; } // Add the unit shown at position to the training queue for all entities in the selection function addTrainingByPosition(position) { var simState = GetSimState(); var playerState = simState.players[Engine.GetPlayerID()]; var selection = g_Selection.toList(); if (!selection.length) return; var trainableEnts = getAllTrainableEntitiesFromSelection(); // 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) + var limits = getEntityLimitAndCount(playerState, trainEntType); // Batch training possible if we can train at least 2 units - var batchTrainingPossible = canBeTrainedCount == undefined || canBeTrainedCount > 1; + var batchTrainingPossible = limits.canBeAddedCount == undefined || limits.canBeAddedCount > 1; var decrement = Engine.HotkeyIsPressed("selection.remove"); if (!decrement) var template = GetTemplateData(trainEntType); 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 (decrement) { batchTrainingCount -= batchIncrementSize; if (batchTrainingCount <= 0) inputState = INPUT_NORMAL; } - else if (canBeTrainedCount == undefined || - canBeTrainedCount > batchTrainingCount * appropriateBuildings.length) + else if (limits.canBeAddedCount == undefined || + limits.canBeAddedCount > batchTrainingCount * appropriateBuildings.length) { if (Engine.GuiInterfaceCall("GetNeededResources", multiplyEntityCosts( template, batchTrainingCount + batchIncrementSize))) return; batchTrainingCount += batchIncrementSize; } - batchTrainingEntityAllowedCount = canBeTrainedCount; + batchTrainingEntityAllowedCount = limits.canBeAddedCount; return; } // Otherwise start a new one else if (!decrement) { flushTrainingBatch(); // fall through to create the new batch } } // Don't start a new batch if decrementing or unable to afford it. if (decrement || Engine.GuiInterfaceCall("GetNeededResources", multiplyEntityCosts(template, batchIncrementSize))) return; inputState = INPUT_BATCHTRAINING; batchTrainingEntities = selection; batchTrainingType = trainEntType; - batchTrainingEntityAllowedCount = canBeTrainedCount; + batchTrainingEntityAllowedCount = limits.canBeAddedCount; 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); + if (limits.entLimit) + buildingsForTraining = buildingsForTraining.slice(0, limits.canBeAddedCount); 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; var currentBatchTrainingCount = 0; if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 && batchTrainingType == trainEntType) { nextBatchTrainingCount = batchTrainingCount; currentBatchTrainingCount = batchTrainingCount; - var canBeTrainedCount = batchTrainingEntityAllowedCount; + var limits = { + "canBeAddedCount": batchTrainingEntityAllowedCount + }; } else { - var [trainEntLimit, trainEntCount, canBeTrainedCount] = - getEntityLimitAndCount(playerState, trainEntType); - var batchSize = Math.min(canBeTrainedCount, batchIncrementSize); + var limits = getEntityLimitAndCount(playerState, trainEntType); } // We need to calculate count after the next increment if it's possible - if (canBeTrainedCount == undefined || - canBeTrainedCount > nextBatchTrainingCount * appropriateBuildings.length) + if (limits.canBeAddedCount == undefined || + limits.canBeAddedCount > 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) + if (limits.canBeAddedCount !== undefined && + limits.canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) { - buildingsCountToTrainFullBatch = Math.floor(canBeTrainedCount / nextBatchTrainingCount); - remainderToTrain = canBeTrainedCount % nextBatchTrainingCount; + buildingsCountToTrainFullBatch = Math.floor(limits.canBeAddedCount / nextBatchTrainingCount); + remainderToTrain = limits.canBeAddedCount % nextBatchTrainingCount; } return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain, currentBatchTrainingCount]; } // 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, 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) return; var entState = GetExtendedEntityState(entity); var playerID = Engine.GetPlayerID(); if (!entState.player == playerID && !g_DevSettings.controlAll) return; if (entityCommands[commandName]) entityCommands[commandName].execute(entState); } // Performs the specified formation function performFormation(entity, formationTemplate) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "formation", "entities": selection, "name": formationTemplate }); } } // 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": case "breakUp": g_Groups.groups[groupId].reset(); if (action == "save") g_Groups.addEntities(groupId, g_Selection.toList()); 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, "excludeUnits": [] }; 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]); var position = GetEntityState(lastIdleUnit).position; if (position) Engine.CameraMoveTo(position.x, position.z); 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 unloadSelection() { var entities = g_Selection.toList(); var parent = 0; var ents = []; for each (var ent in entities) { var state = GetExtendedEntityState(ent); if (!state || !state.turretParent) continue; if (!parent) { parent = state.turretParent; ents.push(ent); } else if (state.turretParent == parent) ents.push(ent) } if (parent) Engine.PostNetworkCommand({"type": "unload", "entities":ents, "garrisonHolder": parent}); } 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}); } function backToWork() { // Filter out all entities that can't go back to work. var workers = g_Selection.toList().filter(function(e) { var state = GetEntityState(e); return (state && state.unitAI && state.unitAI.hasWorkOrders); }); Engine.PostNetworkCommand({"type": "back-to-work", "entities": workers}); } function removeGuard() { // Filter out all entities that are currently guarding/escorting. var entities = g_Selection.toList().filter(function(e) { var state = GetEntityState(e); return (state && state.unitAI && state.unitAI.isGuarding); }); Engine.PostNetworkCommand({"type": "remove-guard", "entities": entities}); } function increaseAlertLevel() { var entities = g_Selection.toList().filter(function(e) { var state = GetEntityState(e); return (state && state.alertRaiser && state.alertRaiser.canIncreaseLevel); }); Engine.PostNetworkCommand({"type": "increase-alert-level", "entities": entities}); } function endOfAlert() { var entities = g_Selection.toList().filter(function(e) { var state = GetEntityState(e); return (state && state.alertRaiser && state.alertRaiser.hasRaisedAlert); }); Engine.PostNetworkCommand({"type": "alert-end", "entities": entities}); } function clearSelection() { if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING) { inputState = INPUT_NORMAL; placementSupport.Reset(); } else g_Selection.reset(); preSelectedAction = ACTION_NONE; } Index: ps/trunk/binaries/data/mods/public/gui/session/selection.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 15374) +++ ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 15375) @@ -1,512 +1,530 @@ // Limits selection size const MAX_SELECTION_SIZE = 200; // Alpha value of hovered/mouseover/highlighted selection overlays // (should probably be greater than always visible alpha value, // see CCmpSelectable) const HIGHLIGHTED_ALPHA = 0.75; function _setHighlight(ents, alpha, selected) { if (ents.length) Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities":ents, "alpha":alpha, "selected":selected }); } function _setStatusBars(ents, enabled) { if (ents.length) Engine.GuiInterfaceCall("SetStatusBars", { "entities":ents, "enabled":enabled }); } function _setMotionOverlay(ents, enabled) { if (ents.length) Engine.GuiInterfaceCall("SetMotionDebugOverlay", { "entities":ents, "enabled":enabled }); } function _playSound(ent) { Engine.GuiInterfaceCall("PlaySound", { "name":"select", "entity":ent }); } /** * EntityGroups class for managing grouped entities */ function EntityGroups() { this.groups = {}; this.ents = {}; } EntityGroups.prototype.reset = function() { this.groups = {}; this.ents = {}; }; EntityGroups.prototype.add = function(ents) { for each (var ent in ents) { if (!this.ents[ent]) { var entState = GetEntityState(ent); // When this function is called during group rebuild, deleted // entities will not yet have been removed, so entities might // still be present in the group despite not existing. if (!entState) continue; var templateName = entState.template; var key = GetTemplateData(templateName).selectionGroupName || templateName; + // TODO ugly hack, just group them by player too. // Prefix garrisoned unit's selection name with the player they belong to var index = templateName.indexOf("&"); if (index != -1 && key.indexOf("&") == -1) key = templateName.slice(0, index+1) + key; if (this.groups[key]) this.groups[key] += 1; else this.groups[key] = 1; this.ents[ent] = key; } } }; EntityGroups.prototype.removeEnt = function(ent) { var templateName = this.ents[ent]; // Remove the entity delete this.ents[ent]; this.groups[templateName]--; // Remove the entire group if (this.groups[templateName] == 0) delete this.groups[templateName]; }; EntityGroups.prototype.rebuildGroup = function(renamed) { var oldGroup = this.ents; this.reset(); var toAdd = []; for (var ent in oldGroup) toAdd.push(renamed[ent] ? renamed[ent] : +ent); this.add(toAdd); } EntityGroups.prototype.getCount = function(templateName) { return this.groups[templateName]; }; EntityGroups.prototype.getTotalCount = function() { var totalCount = 0; for each (var group in this.groups) { totalCount += group; } return totalCount; }; EntityGroups.prototype.getTemplateNames = function() { var templateNames = []; for (var templateName in this.groups) templateNames.push(templateName); //Preserve order even when shuffling units around //Can be optimized by moving the sorting elsewhere templateNames.sort(); return templateNames; }; EntityGroups.prototype.getEntsByName = function(templateName) { var ents = []; for (var ent in this.ents) { if (this.ents[ent] == templateName) ents.push(+ent); } return ents; }; /** + * get a list of entities grouped by templateName + */ +EntityGroups.prototype.getEntsGrouped = function() +{ + var templateNames = this.getTemplateNames(); + var list = []; + for (var t of templateNames) + { + list.push({ + "ents": this.getEntsByName(t), + "template": t, + }); + } + return list; +}; + +/** * Gets all ents in every group except ones of the specified group */ EntityGroups.prototype.getEntsByNameInverse = function(templateName) { var ents = []; for (var ent in this.ents) { if (this.ents[ent] != templateName) ents.push(+ent); } return ents; }; /** * EntitySelection class for managing the entity selection list and the primary selection */ function EntitySelection() { // Private properties: //-------------------------------- this.selected = {}; // { id:id, id:id, ... } for each selected entity ID 'id' // { id:id, ... } for mouseover-highlighted entity IDs in these, the key is a string and the value is an int; // we want to use the int form wherever possible since it's more efficient to send to the simulation code) this.highlighted = {}; this.motionDebugOverlay = false; // Public properties: //-------------------------------- this.dirty = false; // set whenever the selection has changed this.groups = new EntityGroups(); } /** * Deselect everything but entities of the chosen type if the modifier is true otherwise deselect just the chosen entity */ EntitySelection.prototype.makePrimarySelection = function(templateName, modifierKey) { var selection = this.toList(); var template = GetTemplateData(templateName); var key = template.selectionGroupName || templateName; var ents = []; if (modifierKey) ents = this.groups.getEntsByNameInverse(key); else ents = this.groups.getEntsByName(key); this.reset(); this.addList(ents); } /** * Get a list of the template names */ EntitySelection.prototype.getTemplateNames = function() { var templateNames = []; var ents = this.toList(); for each (var ent in ents) { var entState = GetEntityState(ent); if (entState) templateNames.push(entState.template); } return templateNames; } /** * Update the selection to take care of changes (like units that have been killed) */ EntitySelection.prototype.update = function() { var changed = false; this.checkRenamedEntities(); for each (var ent in this.selected) { var entState = GetEntityState(ent); // Remove deleted units if (!entState) { delete this.selected[ent]; this.groups.removeEnt(ent); changed = true; continue; } // Remove non-visible units (e.g. moved back into fog-of-war) if (entState.visibility == "hidden") { // Disable any highlighting of the disappeared unit _setHighlight([ent], 0, false); _setStatusBars([ent], false); _setMotionOverlay([ent], false); delete this.selected[ent]; this.groups.removeEnt(ent); changed = true; continue; } } if (changed) this.onChange(); }; /** * Update selection if some selected entities were renamed * (in case of unit promotion or finishing building structure) */ EntitySelection.prototype.checkRenamedEntities = function() { var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities"); if (renamedEntities.length > 0) { var renamedLookup = {}; for each (var renamedEntity in renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; // Reconstruct the selection if at least one entity has been renamed. for each (var renamedEntity in renamedEntities) { if (this.selected[renamedEntity.entity]) { this.rebuildSelection(renamedLookup); break; } } } } /** * Add entities to selection. Play selection sound unless quiet is true */ EntitySelection.prototype.addList = function(ents, quiet) { var selection = this.toList(); var playerID = Engine.GetPlayerID(); // If someone else's player is the sole selected unit, don't allow adding to the selection if (!g_DevSettings.controlAll && selection.length == 1) { var firstEntState = GetEntityState(selection[0]); if (firstEntState && firstEntState.player != playerID) return; } // Allow selecting things not belong to this player (enemy, ally, gaia) var allowUnownedSelect = g_DevSettings.controlAll || (ents.length == 1 && selection.length == 0); var i = 1; var added = []; for each (var ent in ents) { // Only add entities we own to our selection var entState = GetEntityState(ent); if (!this.selected[ent] && (selection.length + i) <= MAX_SELECTION_SIZE && (allowUnownedSelect || (entState && entState.player == playerID))) { added.push(ent); this.selected[ent] = ent; i++; } } _setHighlight(added, 1, true); _setStatusBars(added, true); _setMotionOverlay(added, this.motionDebugOverlay); if (added.length) { // Play the sound if the entity is controllable by us or Gaia-owned. var owner = GetEntityState(added[0]).player; if (!quiet && (owner == playerID || owner == 0 || g_DevSettings.controlAll)) _playSound(added[0]); } this.groups.add(this.toList()); // Create Selection Groups this.onChange(); }; EntitySelection.prototype.removeList = function(ents) { var removed = []; for each (var ent in ents) { if (this.selected[ent]) { this.groups.removeEnt(ent); removed.push(ent); delete this.selected[ent]; } } _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setMotionOverlay(removed, false); this.onChange(); }; EntitySelection.prototype.reset = function() { _setHighlight(this.toList(), 0, false); _setStatusBars(this.toList(), false); _setMotionOverlay(this.toList(), false); this.selected = {}; this.groups.reset(); this.onChange(); }; EntitySelection.prototype.rebuildSelection = function(renamed) { var oldSelection = this.selected; this.reset(); var toAdd = []; for each (var ent in oldSelection) toAdd.push(renamed[ent] ? renamed[ent] : ent); this.addList(toAdd, true); // don't play selection sounds } EntitySelection.prototype.toList = function(){ var ents = []; for each (var ent in this.selected) ents.push(ent); return ents; }; EntitySelection.prototype.setHighlightList = function(ents) { var highlighted = {}; for each (var ent in ents) highlighted[ent] = ent; var removed = []; var added = []; // Remove highlighting for the old units that are no longer highlighted // (excluding ones that are actively selected too) for each (var ent in this.highlighted) if (!highlighted[ent] && !this.selected[ent]) removed.push(+ent); // Add new highlighting for units that aren't already highlighted for each (var ent in ents) if (!this.highlighted[ent] && !this.selected[ent]) added.push(+ent); _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setHighlight(added, HIGHLIGHTED_ALPHA, true); _setStatusBars(added, true); // Store the new highlight list this.highlighted = highlighted; }; EntitySelection.prototype.SetMotionDebugOverlay = function(enabled) { this.motionDebugOverlay = enabled; _setMotionOverlay(this.toList(), enabled); }; EntitySelection.prototype.onChange = function() { this.dirty = true; if (this == g_Selection) onSelectionChange(); } /** * Cache some quantities which depends only on selection */ var g_Selection = new EntitySelection(); var g_canMoveIntoFormation = {}; var g_allBuildableEntities = undefined; var g_allTrainableEntities = undefined; // Reset cached quantities function onSelectionChange() { g_canMoveIntoFormation = {}; g_allBuildableEntities = undefined; g_allTrainableEntities = undefined; } /** * EntityGroupsContainer class for managing grouped entities */ function EntityGroupsContainer() { this.groups = []; for (var i = 0; i < 10; ++i) { this.groups[i] = new EntityGroups(); } } EntityGroupsContainer.prototype.addEntities = function(groupName, ents) { for each (var ent in ents) { for each (var group in this.groups) { if (ent in group.ents) { group.removeEnt(ent); } } } this.groups[groupName].add(ents); } EntityGroupsContainer.prototype.update = function() { this.checkRenamedEntities(); for each (var group in this.groups) { for (var ent in group.ents) { var entState = GetEntityState(+ent); // Remove deleted units if (!entState) { group.removeEnt(ent); } } } } /** * Update control group if some entities in the group were renamed * (in case of unit promotion or finishing building structure) */ EntityGroupsContainer.prototype.checkRenamedEntities = function() { var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities"); if (renamedEntities.length > 0) { var renamedLookup = {}; for each (var renamedEntity in renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; for each (var group in this.groups) { for each (var renamedEntity in renamedEntities) { // Reconstruct the group if at least one entity has been renamed. if (renamedEntity.entity in group.ents) { group.rebuildGroup(renamedLookup); break; } } } } } var g_Groups = new EntityGroupsContainer(); Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 15374) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 15375) @@ -1,435 +1,432 @@ function layoutSelectionSingle() { Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false; Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; } function layoutSelectionMultiple() { Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; } function getResourceTypeDisplayName(resourceType) { var resourceCode = resourceType["generic"]; var displayName = ""; if (resourceCode == "treasure") displayName = getLocalizedResourceName(resourceType["specific"], "firstWord"); else displayName = getLocalizedResourceName(resourceCode, "firstWord"); return displayName; } // Fills out information that most entities have function displaySingle(entState, template) { // Get general unit and player data var specificName = template.name.specific; var genericName = template.name.generic != template.name.specific ? template.name.generic : ""; // If packed, add that to the generic name (reduces template clutter) if (genericName && template.pack && template.pack.state == "packed") genericName = sprintf(translate("%(genericName)s — Packed"), { genericName: genericName }); var playerState = g_Players[entState.player]; var civName = g_CivData[playerState.civ].Name; var civEmblem = g_CivData[playerState.civ].Emblem; var playerName = playerState.name; var playerColor = playerState.color.r + " " + playerState.color.g + " " + playerState.color.b + " 128"; // Indicate disconnected players by prefixing their name if (g_Players[entState.player].offline) { playerName = sprintf(translate("[OFFLINE] %(player)s"), { player: playerName }); } // Rank if (entState.identity && entState.identity.rank && entState.identity.classes) { Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), { rank: translateWithContext("Rank", entState.identity.rank) }); Engine.GetGUIObjectByName("rankIcon").sprite = getRankIconSprite(entState); Engine.GetGUIObjectByName("rankIcon").hidden = false; } else { Engine.GetGUIObjectByName("rankIcon").hidden = true; Engine.GetGUIObjectByName("rankIcon").tooltip = ""; } // Hitpoints if (entState.hitpoints) { var unitHealthBar = Engine.GetGUIObjectByName("healthBar"); var healthSize = unitHealthBar.size; healthSize.rright = 100*Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints)); unitHealthBar.size = healthSize; Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), { hitpoints: Math.ceil(entState.hitpoints), maxHitpoints: entState.maxHitpoints }); Engine.GetGUIObjectByName("healthSection").hidden = false; } else { Engine.GetGUIObjectByName("healthSection").hidden = true; } // TODO: Stamina var player = Engine.GetPlayerID(); if (entState.stamina && (entState.player == player || g_DevSettings.controlAll)) Engine.GetGUIObjectByName("staminaSection").hidden = false; else Engine.GetGUIObjectByName("staminaSection").hidden = true; // Experience if (entState.promotion) { var experienceBar = Engine.GetGUIObjectByName("experienceBar"); var experienceSize = experienceBar.size; experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req))); experienceBar.size = experienceSize; if (entState.promotion.curr < entState.promotion.req) Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s / %(required)s"), { experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]", current: Math.floor(entState.promotion.curr), required: entState.promotion.req }); else Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s"), { experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]", current: Math.floor(entState.promotion.curr) }); Engine.GetGUIObjectByName("experience").hidden = false; } else { Engine.GetGUIObjectByName("experience").hidden = true; } // Resource stats if (entState.resourceSupply) { var resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol sprintf(translate("%(amount)s / %(max)s"), { amount: Math.ceil(+entState.resourceSupply.amount), max: entState.resourceSupply.max }); var resourceType = getResourceTypeDisplayName(entState.resourceSupply.type); var unitResourceBar = Engine.GetGUIObjectByName("resourceBar"); var resourceSize = unitResourceBar.size; resourceSize.rright = entState.resourceSupply.isInfinite ? 100 : 100 * Math.max(0, Math.min(1, +entState.resourceSupply.amount / +entState.resourceSupply.max)); unitResourceBar.size = resourceSize; Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), { resource: resourceType }); Engine.GetGUIObjectByName("resourceStats").caption = resources; if (entState.hitpoints) Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("staminaSection").size; else Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("healthSection").size; Engine.GetGUIObjectByName("resourceSection").hidden = false; } else { Engine.GetGUIObjectByName("resourceSection").hidden = true; } // Resource carrying if (entState.resourceCarrying && entState.resourceCarrying.length) { // We should only be carrying one resource type at once, so just display the first var carried = entState.resourceCarrying[0]; Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+carried.type+".png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { amount: carried.amount, max: carried.max }); Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = ""; } // Use the same indicators for traders else if (entState.trader && entState.trader.goods.amount) { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+entState.trader.goods.type+".png"; var totalGain = entState.trader.goods.amount.traderGain; if (entState.trader.goods.amount.market1Gain) totalGain += entState.trader.goods.amount.market1Gain; if (entState.trader.goods.amount.market2Gain) totalGain += entState.trader.goods.amount.market2Gain; Engine.GetGUIObjectByName("resourceCarryingText").caption = totalGain; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Gain: %(amount)s"), { amount: getTradingTooltip(entState.trader.goods.amount) }); } // And for number of workers else if (entState.foundation) { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.foundation.numBuilders + " "; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Number of builders"); } else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints)) { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { amount: entState.resourceSupply.gatherers.length, max: entState.resourceSupply.maxGatherers }) + " "; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Current/max gatherers"); } else { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = true; Engine.GetGUIObjectByName("resourceCarryingText").hidden = true; } // Set Player details Engine.GetGUIObjectByName("specific").caption = specificName; Engine.GetGUIObjectByName("player").caption = playerName; Engine.GetGUIObjectByName("playerColorBackground").sprite = "colour: " + playerColor; if (genericName) { Engine.GetGUIObjectByName("generic").caption = sprintf(translate("(%(genericName)s)"), { genericName: genericName }); } else { Engine.GetGUIObjectByName("generic").caption = ""; } if ("gaia" != playerState.civ) { Engine.GetGUIObjectByName("playerCivIcon").sprite = "stretched:grayscale:" + civEmblem; Engine.GetGUIObjectByName("player").tooltip = civName; } else { Engine.GetGUIObjectByName("playerCivIcon").sprite = ""; Engine.GetGUIObjectByName("player").tooltip = ""; } // Icon image if (template.icon) { Engine.GetGUIObjectByName("icon").sprite = "stretched:session/portraits/" + template.icon; } else { // TODO: we should require all entities to have icons, so this case never occurs Engine.GetGUIObjectByName("icon").sprite = "bkFillBlack"; } var armorLabel = "[font=\"sans-bold-13\"]" + translate("Armor:") + "[/font]" var armorString = sprintf(translate("%(label)s %(details)s"), { label: armorLabel, details: armorTypeDetails(entState.armour) }); // Attack and Armor if ("attack" in entState && entState.attack) { // Rate if (entState.buildingAI) var rateLabel = "[font=\"sans-bold-13\"]" + translate("Interval:") + "[/font]"; else var rateLabel = "[font=\"sans-bold-13\"]" + translate("Rate:") + "[/font]"; var rate = sprintf(translate("%(label)s %(details)s"), { label: rateLabel, details: attackRateDetails(entState) }); var attack; var label = "[font=\"sans-bold-13\"]" + getAttackTypeLabel(entState.attack.type) + "[/font]" if (entState.attack.type == "Ranged") { var realRange = entState.attack.elevationAdaptedRange; var range = entState.attack.maxRange; var rangeLabel = "[font=\"sans-bold-13\"]" + translate("Range:") + "[/font]" var relativeRange = Math.round((realRange - range)); var meters = "[font=\"sans-10\"][color=\"orange\"]" + translate("meters") + "[/color][/font]"; if (relativeRange > 0) attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s (%(relative)s), %(rate)s"), { label: label, details: damageTypeDetails(entState.attack), rangeLabel: rangeLabel, range: Math.round(range), meters: meters, relative: "+" + relativeRange, rate: rate }); else if (relativeRange < 0) attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s (%(relative)s), %(rate)s"), { label: label, details: damageTypeDetails(entState.attack), rangeLabel: rangeLabel, range: Math.round(range), meters: meters, relative: relativeRange, rate: rate }); else // don't show when it's 0 attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s, %(rate)s"), { label: label, details: damageTypeDetails(entState.attack), rangeLabel: rangeLabel, range: Math.round(range), meters: meters, rate: rate }); } else { attack = sprintf(translate("%(label)s %(details)s, %(rate)s"), { label: label, details: damageTypeDetails(entState.attack), rate: rate }); } Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n" + armorString; } else { Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = armorString; } // Icon Tooltip var iconTooltip = ""; if (genericName) iconTooltip = "[font=\"sans-bold-16\"]" + genericName + "[/font]"; if (template.visibleIdentityClasses && template.visibleIdentityClasses.length) { iconTooltip += "\n[font=\"sans-bold-13\"]" + translate("Classes:") + "[/font] "; iconTooltip += "[font=\"sans-13\"]" + translate(template.visibleIdentityClasses[0]) ; for (var i = 1; i < template.visibleIdentityClasses.length; i++) iconTooltip += ", " + translate(template.visibleIdentityClasses[i]); iconTooltip += "[/font]"; } if (template.auras) { for (var auraName in template.auras) { iconTooltip += "\n[font=\"sans-bold-13\"]" + translate(auraName) + "[/font]"; if (template.auras[auraName]) iconTooltip += ": " + translate(template.auras[auraName]); } } if (template.tooltip) iconTooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]"; Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltip; // Unhide Details Area Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false; Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; } // Fills out information for multiple entities function displayMultiple(selection, template) { var averageHealth = 0; var maxHealth = 0; for (var i = 0; i < selection.length; i++) { var entState = GetEntityState(selection[i]) if (entState) { if (entState.hitpoints) { averageHealth += entState.hitpoints; maxHealth += entState.maxHitpoints; } } } if (averageHealth > 0) { var unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple"); var healthSize = unitHealthBar.size; healthSize.rtop = 100-100*Math.max(0, Math.min(1, averageHealth / maxHealth)); unitHealthBar.size = healthSize; var hitpointsLabel = "[font=\"sans-bold-13\"]" + translate("Hitpoints:") + "[/font]" var hitpoints = sprintf(translate("%(label)s %(current)s / %(max)s"), { label: hitpointsLabel, current: averageHealth, max: maxHealth }); var healthMultiple = Engine.GetGUIObjectByName("healthMultiple"); healthMultiple.tooltip = hitpoints; healthMultiple.hidden = false; } else { Engine.GetGUIObjectByName("healthMultiple").hidden = true; } // TODO: Stamina // Engine.GetGUIObjectByName("staminaBarMultiple"); Engine.GetGUIObjectByName("numberOfUnits").caption = selection.length; // Unhide Details Area Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; } // Updates middle entity Selection Details Panel function updateSelectionDetails() { var supplementalDetailsPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails"); var detailsPanel = Engine.GetGUIObjectByName("selectionDetails"); var commandsPanel = Engine.GetGUIObjectByName("unitCommands"); g_Selection.update(); var selection = g_Selection.toList(); if (selection.length == 0) { Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; hideUnitCommands(); supplementalDetailsPanel.hidden = true; detailsPanel.hidden = true; commandsPanel.hidden = true; return; } /* If the unit has no data (e.g. it was killed), don't try displaying any data for it. (TODO: it should probably be removed from the selection too; also need to handle multi-unit selections) */ var entState = GetExtendedEntityState(selection[0]); if (!entState) return; var template = GetTemplateData(entState.template); // Fill out general info and display it if (selection.length == 1) displaySingle(entState, template); else displayMultiple(selection, template); // Show basic details. detailsPanel.hidden = false; if (g_IsObserver) { // Observers don't need these displayed. supplementalDetailsPanel.hidden = true; commandsPanel.hidden = true; } else { // Fill out commands panel for specific unit selected (or first unit of primary group) updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection); - // Show panels - supplementalDetailsPanel.hidden = false; - commandsPanel.hidden = false; } } Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 15374) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 15375) @@ -1,610 +1,935 @@ /** * Contains the layout and button settings per selection panel * - * addData is called first, and can be used to abort going any furter - * by returning false. Else it should always return true. + * getItems returns a list of basic items used to fill the panel. + * This method is obligated. If the items list is empty, the panel + * won't be rendered. + * + * Then there's a loop over all items provided. In the loop, + * the item and some other standard data is added to a data object. * - * addData is used to add data to the data object that can be used in the - * content setter functions. + * The standard data is + * var data = { + * "i": index + * "item": item coming from the getItems function + * "selection": list of currently selected items + * "playerState": playerState + * "unitEntState": first selected entity state + * "rowLength": rowLength + * "numberOfItems": number of items that will be processed + * "button": gui Button object + * "affordableMask": gui Unaffordable overlay + * "icon": gui Icon object + * "guiSelection": gui button Selection overlay + * "countDisplay": gui caption space + * }; + * + * Then, addData is called, and can be used to abort the processing + * of the current item by returning false. + * It should return true if you want the panel to be filled. + * + * addData is used to add data to the data object on top + * (or instead of) the standard data. + * addData is not obligated, the function will just continue + * with the content setters if no addData is present. + * + * After the addData, all functions starting with "set" are called. + * These are used to set various parts of content. */ var g_SelectionPanels = {}; +// BARTER +g_SelectionPanels.Barter = { + "maxNumberOfItems": 4, + "rowLength": 4, + "getItems": function(unitEntState, selection) + { + if (!unitEntState.barterMarket) + return []; + // ["food", "wood", "stone", "metal"] + return BARTER_RESOURCES; + }, + "addData": function(data) + { + // data.item is the resource name in this case + data.button = {}; + data.icon = {}; + data.amount = {}; + for (var a of BARTER_ACTIONS) + { + data.button[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Button["+data.i+"]"); + data.icon[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Icon["+data.i+"]"); + data.amount[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Amount["+data.i+"]"); + } + data.selection = Engine.GetGUIObjectByName("unitBarterSellSelection["+data.i+"]"); + data.affordableMask = Engine.GetGUIObjectByName("unitBarterSellUnaffordable["+data.i+"]"); + + data.amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL; + if (Engine.HotkeyIsPressed("session.massbarter")) + data.amountToSell *= BARTER_BUNCH_MULTIPLIER; + data.isSelected = data.item == g_barterSell; + return true; + }, + "setCountDisplay": function(data) + { + data.amount.Sell.caption = "-" + data.amountToSell; + var sellPrice = data.unitEntState.barterMarket.prices["sell"][g_barterSell]; + var buyPrice = data.unitEntState.barterMarket.prices["buy"][data.item]; + data.amount.Buy.caption = "+" + Math.round(sellPrice / buyPrice * data.amountToSell); + }, + "setTooltip": function(data) + { + data.button.Buy.tooltip = sprintf(translate("Buy %(resource)s"), {"resource": translate(data.item)}); + data.button.Sell.tooltip = sprintf(translate("Sell %(resource)s"), {"resource": translate(data.item)}); + }, + "setAction": function(data) + { + data.button.Sell.onPress = function() { g_barterSell = data.item; }; + var exchangeResourcesParameters = { + "sell": g_barterSell, + "buy": data.item, + "amount": data.amountToSell + }; + data.button.Buy.onPress = function() { exchangeResources(exchangeResourcesParameters); }; + }, + "setGraphics": function(data) + { + var grayscale = data.isSelected ? "grayscale:" : ""; + data.button.Buy.hidden = data.isSelected; + data.button.Sell.hidden = false; + for each (var icon in data.icon) + icon.sprite = "stretched:"+grayscale+"session/icons/resources/" + data.item + ".png"; + + var neededRes = {}; + neededRes[data.item] = data.amountToSell; + if (Engine.GuiInterfaceCall("GetNeededResources", neededRes)) + data.affordableMask.hidden = false; + else + data.affordableMask.hidden = true; + data.selection.hidden = !data.isSelected; + }, + "setPosition": function(data) + { + setPanelObjectPosition(data.button.Sell, data.i, data.rowLength); + setPanelObjectPosition(data.button.Buy, data.i + data.rowLength, data.rowLength); + }, +}, + // COMMAND g_SelectionPanels.Command = { "maxNumberOfItems": 6, + "getItems": function(unitEntState) + { + return getEntityCommandsList(unitEntState) + }, "setTooltip": function(data) { if (data.item.tooltip) data.button.tooltip = data.item.tooltip else data.button.tooltip = toTitleCase(data.item.name); }, + "setAction": function(data) + { + data.button.onPress = function() { data.item.callback ? data.item.callback(data.item) : performCommand(data.unitEntState.id, data.item.name); }; + }, "setCountDisplay": function(data) { - var count = 0; - if (data.item.name == "unload-all") - count = data.garrisonGroups.getTotalCount(); - data.countDisplay.caption = count || ""; + data.countDisplay.caption = data.item.count || ""; }, "setGraphics": function(data) { data.icon.sprite = "stretched:session/icons/" + data.item.icon; }, "setPosition": function(data) { var size = data.button.size; // count on square buttons, so size.bottom is the width too var spacer = size.bottom + 1; // relative to the center ( = 50%) size.rleft = size.rright = 50; // offset from the center calculation size.left = (data.i - data.numberOfItems/2) * spacer; size.right = size.left + size.bottom; data.button.size = size; }, }; // CONSTRUCTION g_SelectionPanels.Construction = { "maxNumberOfItems": 24, + "conflictsWith": ["Gate", "Pack", "Training"], + "getItems": function() + { + return getAllBuildableEntitiesFromSelection(); + }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) // abort if no template return false; data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", data.template.requiredTechnology); if (data.template.cost) { var totalCost = multiplyEntityCosts(data.template, 1); data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", totalCost); } + data.limits = getEntityLimitAndCount(data.playerState, data.entType); return true; }, + "setAction": function(data) + { + data.button.onPress = function () { startBuildingPlacement(data.item, data.playerState); }; + }, "setTooltip": function(data) { var tooltip = getEntityNamesFormatted(data.template); tooltip += getVisibleEntityClassesFormatted(data.template); if (data.template.tooltip) tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(data.template); tooltip += getPopulationBonusTooltip(data.template); - var limits = getEntityLimitAndCount(data.playerState, data.entType); - data.entLimit = limits[0]; - data.entCount = limits[1]; - data.canBeAddedCount = limits[2]; - data.entLimitChangers = limits[3]; - - tooltip += formatLimitString(data.entLimit, data.entCount, data.entLimitChangers); + tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers); if (!data.technologyEnabled) { var techName = getEntityNames(GetTechnologyData(data.template.requiredTechnology)); tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { technology: techName }); } if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); data.button.tooltip = tooltip; return true; }, "setGraphics": function(data) { var grayscale = ""; - if (!data.technologyEnabled || data.canBeAddedCount == 0) + if (!data.technologyEnabled || data.limits.canBeAddedCount == 0) { data.button.enabled = false; grayscale = "grayscale:"; data.affordableMask.hidden = false; data.affordableMask.sprite = "colour: 0 0 0 127"; } else if (data.neededResources) { data.button.enabled = false; data.affordableMask.hidden = false; data.affordableMask.sprite = resourcesToAlphaMask(data.neededResources); } if (data.template.icon) data.icon.sprite = "stretched:" + grayscale + "session/portraits/" + data.template.icon; }, }; // FORMATION g_SelectionPanels.Formation = { "maxNumberOfItems": 16, "rowLength": 4, + "conflictsWith": ["Garrison"], + "getItems": function(unitEntState) + { + if (!hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal")) + return []; + return Engine.GuiInterfaceCall("GetAvailableFormations"); + }, "addData": function(data) { data.formationInfo = Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", {"templateName": data.item}); data.formationOk = canMoveSelectionIntoFormation(data.item); data.formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "ents": data.selection, "formationTemplate": data.item }); return true; }, + "setAction": function(data) + { + data.button.onPress = function() { performFormation(data.unitEntState.id, data.item); }; + }, "setTooltip": function(data) { - var tooltip = translate(data.formationInfo.name); + var tooltip = translate(data.formationInfo.name); if (!data.formationOk && data.formationInfo.tooltip) tooltip += "\n" + "[color=\"red\"]" + translate(data.formationInfo.tooltip) + "[/color]"; data.button.tooltip = tooltip; }, "setGraphics": function(data) { data.button.enabled = data.formationOk; var grayscale = data.formationOk ? "" : "grayscale:"; data.guiSelection.hidden = !data.formationSelected; data.icon.sprite = "stretched:"+grayscale+"session/icons/"+data.formationInfo.icon; }, }; // GARRISON g_SelectionPanels.Garrison = { "maxNumberOfItems": 12, "rowLength": 4, + "getItems": function(unitEntState, selection) + { + if (!unitEntState.garrisonHolder) + return []; + var groups = new EntityGroups(); + for (var ent of selection) + { + var state = GetEntityState(ent); + if (state.garrisonHolder) + groups.add(state.garrisonHolder.entities) + } + return groups.getEntsGrouped(); + }, "addData": function(data) { - data.entType = data.item; + data.entType = data.item.template; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.name = getEntityNames(data.template); - data.count = data.garrisonGroups.getCount(data.item); + data.count = data.item.ents.length; return true; }, + "setAction": function(data) + { + data.button.onPress = function() { unloadTemplate(data.item.template); }; + }, "setTooltip": function(data) { var tooltip = sprintf(translate("Unload %(name)s"), { name: data.name }) + "\n"; tooltip += translate("Single-click to unload 1. Shift-click to unload all of this type."); data.button.tooltip = tooltip; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.count || ""; }, "setGraphics": function(data) { var grayscale = ""; - var ents = data.garrisonGroups.getEntsByName(data.item); + var ents = data.item.ents; var entplayer = GetEntityState(ents[0]).player; data.button.sprite = "colour: " + rgbToGuiColor(g_Players[entplayer].color); var player = Engine.GetPlayerID(); if(player != data.unitEntState.player && !g_DevSettings.controlAll) { if (data.entplayer != player) { data.button.enabled = false; grayscale = "grayscale:"; } } data.icon.sprite = "stretched:" + grayscale + "session/portraits/" + data.template.icon; }, }; // GATE g_SelectionPanels.Gate = { "maxNumberOfItems": 8, + "conflictsWith": ["Construction", "Pack", "Training"], + "getItems": function(unitEntState, selection) + { + if (unitEntState.foundation) + return []; + if (!hasClass(unitEntState, "LongWall") && !unitEntState.gate) + return []; + // Allow long wall pieces to be converted to gates + var longWallTypes = {}; + var walls = []; + var gates = []; + for (var i in selection) + { + var state = GetEntityState(selection[i]); + if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template]) + { + var gateTemplate = getWallGateTemplate(state.id); + if (gateTemplate) + { + var tooltipString = GetTemplateDataWithoutLocalization(state.template).gateConversionTooltip; + if (!tooltipString) + { + warn(state.template + " is supposed to be convertable to a gate, but it's missing the GateConversionTooltip in the Identity template"); + tooltipString = ""; + } + walls.push({ + "tooltip": translate(tooltipString), + "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) + { + gates.push({ + "gate": state.gate, + "tooltip": translate("Lock Gate"), + "locked": true, + "callback": function (item) { lockGate(item.locked); } + }); + gates.push({ + "gate": state.gate, + "tooltip": translate("Unlock Gate"), + "locked": false, + "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); + return items; + }, + "setAction": function(data) + { + data.button.onPress = function() {data.item.callback(data.item); }; + }, "setTooltip": function(data) { var tooltip = data.item.tooltip; if (data.item.template) { data.template = GetTemplateData(data.item.template); data.wallCount = data.selection.reduce(function (count, ent) { var state = GetEntityState(ent); if (hasClass(state, "LongWall") && !state.gate) count++; return count; }, 0); tooltip += "\n" + getEntityCostTooltip(data.template, data.wallCount); data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", multiplyEntityCosts(data.template, data.wallCount)); if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); } data.button.tooltip = tooltip; }, "setGraphics": function(data) { data.affordableMask.hidden == data.neededResources ? true : false; var gateIcon; if (data.item.gate) { // If already a gate, show locking actions gateIcon = "icons/lock_" + GATE_ACTIONS[data.item.locked ? 0 : 1] + "ed.png"; if (data.item.gate.locked === undefined) data.guiSelection.hidden = false else data.guiSelection.hidden = data.item.gate.locked != data.item.locked; } else { // otherwise show gate upgrade icon var template = GetTemplateData(data.item.template); if (!template) return; - gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png"; + gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png"; data.guiSelection.hidden = true; } data.icon.sprite = "stretched:session/" + gateIcon; }, }; // PACK g_SelectionPanels.Pack = { "maxNumberOfItems": 8, + "conflictsWith": ["Construction", "Gate", "Training"], + "getItems": function(unitEntState, selection) + { + if (!unitEntState.pack) + return []; + var checks = {}; + for (var ent of selection) + { + var state = GetEntityState(ent); + if (!state.pack) + continue; + if (state.pack.progress == 0) + { + if (!state.pack.packed) + checks.packButton = true; + else if (state.pack.packed) + checks.unpackButton = true; + } + else + { + // Already un/packing - show cancel button + if (!state.pack.packed) + checks.packCancelButton = true; + else if (state.pack.packed) + checks.unpackCancelButton = true; + } + } + var items = []; + if (checks.packButton) + items.push({ "packing": false, "packed": false, "tooltip": translate("Pack"), "callback": function() { packUnit(true); } }); + if (checks.unpackButton) + items.push({ "packing": false, "packed": true, "tooltip": translate("Unpack"), "callback": function() { packUnit(false); } }); + if (checks.packCancelButton) + items.push({ "packing": true, "packed": false, "tooltip": translate("Cancel Packing"), "callback": function() { cancelPackUnit(true); } }); + if (checks.unpackCancelButton) + items.push({ "packing": true, "packed": true, "tooltip": translate("Cancel Unpacking"), "callback": function() { cancelPackUnit(false); } }); + return items; + }, + "setAction": function(data) + { + data.button.onPress = function() {data.item.callback(data.item); }; + }, "setTooltip": function(data) { data.button.tooltip = data.item.tooltip; }, "setGraphics": function(data) { if (data.item.packing) data.icon.sprite = "stretched:session/icons/cancel.png"; else if (data.item.packed) data.icon.sprite = "stretched:session/icons/unpack.png"; else data.icon.sprite = "stretched:session/icons/pack.png"; }, }; // QUEUE g_SelectionPanels.Queue = { "maxNumberOfItems": 16, + "getItems": function(unitEntState) + { + if (!unitEntState.production) + return []; + return unitEntState.production.queue; + }, + "resizePanel": function(numberOfItems, rowLength) + { + var numRows = Math.ceil(numberOfItems / rowLength); + var panel = Engine.GetGUIObjectByName("unitQueuePanel"); + var size = panel.size; + size.top = (UNIT_PANEL_BASE - ((numRows-1)*UNIT_PANEL_HEIGHT)); + panel.size = size; + }, "addData": function(data) { // differentiate between units and techs if (data.item.unitTemplate) { data.entType = data.item.unitTemplate; data.template = GetTemplateData(data.entType); } else if (data.item.technologyTemplate) { data.entType = data.item.technologyTemplate; data.template = GetTechnologyData(data.entType); } data.progress = Math.round(data.item.progress*100) + "%"; return data.template; }, + "setAction": function(data) + { + data.button.onPress = function() { removeFromProductionQueue(data.unitEntState.id, data.item.id); }; + }, "setTooltip": function(data) { var tooltip = getEntityNames(data.template); if (data.item.neededSlots) { tooltip += "\n[color=\"red\"]" + translate("Insufficient population capacity:") + "\n[/color]"; tooltip += sprintf(translate("%(population)s %(neededSlots)s"), { population: getCostComponentDisplayName("population"), neededSlots: data.item.neededSlots }); } data.button.tooltip = tooltip; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.item.count > 1 ? data.item.count : ""; if (data.i == 0) { // Also set the general progress display here // maybe a separate method would be more appropriate Engine.GetGUIObjectByName("queueProgress").caption = data.progress; var guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider["+data.i+"]"); var size = guiObject.size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(data.item.progress * (size.right - size.left)); guiObject.size = size; } }, "setGraphics": function(data) { if (data.template.icon) data.icon.sprite = "stretched:session/portraits/" + data.template.icon; }, }; // RESEARCH g_SelectionPanels.Research = { "maxNumberOfItems": 8, + "getItems": function(unitEntState, selection) + { + if (!unitEntState.production) + return []; + // TODO 8 is the row lenght, make variable + if (getNumberOfRightPanelButtons() > 8 && selection.length > 1) + return []; + return unitEntState.production.technologies; + }, "hideItem": function(i, rowLength) // called when no item is found { Engine.GetGUIObjectByName("unitResearchButton["+i+"]").hidden = true; // We also remove the paired tech and the pair symbol Engine.GetGUIObjectByName("unitResearchButton["+(i+rowLength)+"]").hidden = true; Engine.GetGUIObjectByName("unitResearchPair["+i+"]").hidden = true; }, "addData": function(data) { data.entType = data.item.pair ? [data.item.top, data.item.bottom] : [data.item]; data.template = data.entType.map(GetTechnologyData); // abort if no template found for any of the techs if (!data.template.every(function(v) { return v; })) return false; // index one row below var shiftedIndex = data.i + data.rowLength; data.positions = data.item.pair ? [data.i, shiftedIndex] : [shiftedIndex]; data.positionsToHide = data.item.pair ? [] : [data.i]; // add top buttons to the data data.button = data.positions.map(function(p) { return Engine.GetGUIObjectByName("unitResearchButton["+p+"]"); }); data.buttonsToHide = data.positionsToHide.map(function(p) { return Engine.GetGUIObjectByName("unitResearchButton["+p+"]"); }); data.affordableMask = data.positions.map(function(p) { return Engine.GetGUIObjectByName("unitResearchUnaffordable["+p+"]"); }); data.icon = data.positions.map(function(p) { return Engine.GetGUIObjectByName("unitResearchIcon["+p+"]"); }); data.unchosenIcon = data.positions.map(function(p) { return Engine.GetGUIObjectByName("unitResearchUnchosenIcon["+p+"]"); }); data.neededResources = data.template.map(function(t) { return Engine.GuiInterfaceCall("GetNeededResources", t.cost); }); data.requirementsPassed = data.entType.map(function(e) { return Engine.GuiInterfaceCall("CheckTechnologyRequirements",e); }); data.pair = Engine.GetGUIObjectByName("unitResearchPair["+data.i+"]"); return true; }, "setTooltip": function(data) { for (var i in data.entType) { var tooltip = ""; var template = data.template[i]; tooltip = getEntityNamesFormatted(template); if (template.tooltip) tooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(template); if (!data.requirementsPassed[i]) { tooltip += "\n" + template.requirementsTooltip; if (template.classRequirements) { var player = Engine.GetPlayerID(); var current = GetSimState().players[player].classCounts[template.classRequirements.class] || 0; var remaining = template.classRequirements.number - current; tooltip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), { number: remaining}); } } if (data.neededResources[i]) tooltip += getNeededResourcesTooltip(data.neededResources[i]); data.button[i].tooltip = tooltip; } }, - "setActions": function(data) + "setAction": function(data) { for (var i in data.entType) { // array containing the indices other buttons var others = Object.keys(data.template); others.splice(i, 1); var button = data.button[i]; - button.onpress = (function(e){ return function() { data.callback(e) } })(data.entType[i]); + // as we're in a loop, we need to limit the scope with a closure + // else the last value of the loop will be taken, rather than the current one + button.onpress = (function(template) { return function () { addResearchToQueue(data.unitEntState.id, template); }; })(data.entType[i]); // on mouse enter, show a cross over the other icons button.onmouseenter = (function(others, icons) { return function() { for (var j of others) icons[j].hidden = false; }; })(others, data.unchosenIcon); button.onmouseleave = (function(others, icons) { return function() { for (var j of others) icons[j].hidden = true; }; })(others, data.unchosenIcon); } }, "setGraphics": function(data) { for (var i in data.entType) { var button = data.button[i]; button.hidden = false; var grayscale = ""; if (!data.requirementsPassed[i]) { button.enabled = false; grayscale = "grayscale:"; data.affordableMask[i].hidden = false; data.affordableMask[i].sprite = "colour: 0 0 0 127"; } else if (data.neededResources[i]) { button.enabled = false; data.affordableMask[i].hidden = false; data.affordableMask[i].sprite = resourcesToAlphaMask(data.neededResources[i]); } else { data.affordableMask[i].hidden = true; button.enabled = true; } if (data.template[i].icon) data.icon[i].sprite = "stretched:" + grayscale + "session/portraits/" + data.template[i].icon; } - for (var button of data.buttonsToHide) + for (var button of data.buttonsToHide) button.hidden = true; // show the tech connector data.pair.hidden = data.item.pair == null; }, "setPosition": function(data) { for (var i in data.button) setPanelObjectPosition(data.button[i], data.positions[i], data.rowLength); setPanelObjectPosition(data.pair, data.i, data.rowLength); }, }; // SELECTION g_SelectionPanels.Selection = { "maxNumberOfItems": 16, "rowLength": 4, + "getItems": function(unitEntState, selection) + { + if (selection.length < 2) + return []; + return g_Selection.groups.getTemplateNames(); + }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.name = getEntityNames(data.template); data.count = g_Selection.groups.getCount(data.item); return true; }, "setTooltip": function(data) { data.button.tooltip = data.name; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.count || ""; }, - "setActions": function(data) + "setAction": function(data) { - data.button.onpressright = (function(e){return function() {data.callback(e, true) } })(data.item); - data.button.onpress = (function(e){ return function() {data.callback(e, false) } })(data.item); + data.button.onpressright = function() { changePrimarySelectionGroup(data.item, true); }; + data.button.onpress = function() { changePrimarySelectionGroup(data.item, false); }; }, "setGraphics": function(data) { if (data.template.icon) data.icon.sprite = "stretched:session/portraits/" + data.template.icon; }, }; // STANCE g_SelectionPanels.Stance = { "maxNumberOfItems": 5, + "getItems": function(unitEntState) + { + if (!unitEntState.unitAI || !hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal")) + return []; + return unitEntState.unitAI.possibleStances; + }, "addData": function(data) { data.stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", { "ents": data.selection, "stance": data.item }); return true; }, + "setAction": function(data) + { + data.button.onPress = function() { performStance(data.unitEntState, data.item); }; + }, "setTooltip": function(data) { data.button.tooltip = getStanceDisplayName(data.item); }, "setGraphics": function(data) { data.guiSelection.hidden = !data.stanceSelected; data.icon.sprite = "stretched:session/icons/stances/"+data.item+".png"; }, }; // TRAINING g_SelectionPanels.Training = { "maxNumberOfItems": 24, + "conflictsWith": ["Construction", "Gate", "Pack"], + "getItems": function() + { + return getAllTrainableEntitiesFromSelection(); + }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", data.template.requiredTechnology); var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = getTrainingBatchStatus(data.playerState, data.unitEntState.id, data.entType, data.selection); data.buildingsCountToTrainFullBatch = buildingsCountToTrainFullBatch; data.fullBatchSize = fullBatchSize; data.remainderBatch = remainderBatch; data.trainNum = 1; if (Engine.HotkeyIsPressed("session.batchtrain")) data.trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; if (data.template.cost) { var totalCosts = multiplyEntityCosts(data.template, data.trainNum); data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", totalCosts); } return true; }, + "setAction": function(data) + { + data.button.onPress = function() { addTrainingToQueue(data.selection, data.item, data.playerState); }; + }, "setCountDisplay": function(data) { var count = ""; if (data.trainNum > 1) count = data.trainNum; data.countDisplay.caption = count; }, "setTooltip": function(data) { var tooltip = ""; var key = Engine.ConfigDB_GetValue("user", "hotkey.session.queueunit." + (data.i + 1)); if (key) tooltip += "[color=\"255 251 131\"][font=\"sans-bold-16\"][" + key + "][/font][/color] "; tooltip += getEntityNamesFormatted(data.template); tooltip += getVisibleEntityClassesFormatted(data.template); if (data.template.auras) { for (var auraName in data.template.auras) { tooltip += "\n[font=\"sans-bold-13\"]" + translate(auraName) + "[/font]"; if (data.template.auras[auraName]) tooltip += ": " + translate(data.template.auras[auraName]); } } if (data.template.tooltip) tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(data.template, data.trainNum, data.unitEntState.id); - // TODO make the getEntityLimitAndCount method return something nicer than an array - var limits = getEntityLimitAndCount(data.playerState, data.entType); - data.entLimit = limits[0]; - data.entCount = limits[1]; - data.canBeAddedCount = limits[2]; - data.entLimitChangers = limits[3]; + data.limits = getEntityLimitAndCount(data.playerState, data.entType); - tooltip += formatLimitString(data.entLimit, data.entCount, data.entLimitChangers); + tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers); if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true") { if (data.template.health) tooltip += "\n[font=\"sans-bold-13\"]" + translate("Health:") + "[/font] " + data.template.health; if (data.template.attack) tooltip += "\n" + getEntityAttack(data.template); if (data.template.armour) tooltip += "\n[font=\"sans-bold-13\"]" + translate("Armor:") + "[/font] " + armorTypesToText(data.template.armour); if (data.template.speed) tooltip += "\n" + getEntitySpeed(data.template); } tooltip += "[color=\"255 251 131\"]" + formatBatchTrainingString(data.buildingsCountToTrainFullBatch, data.fullBatchSize, data.remainderBatch) + "[/color]"; if (!data.technologyEnabled) { var techName = getEntityNames(GetTechnologyData(data.template.requiredTechnology)); tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { technology: techName }); } if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); data.button.tooltip = tooltip; }, // disable and enable buttons in the same way as when you do for the construction "setGraphics": g_SelectionPanels.Construction.setGraphics, }; + + +/** + * If two panels need the same space, so they collide, + * the one appearing first in the order is rendered. + * + * Note that the panel needs to appear in the list to get rendered. + */ +var g_PanelsOrder = [ + // LEFT PANE + "Barter", // must always be visible on markets + "Garrison", // more important than Formation, as you want to see the garrisoned units in ships + "Formation", + "Stance", // normal together with formation + + // RIGHT PANE + "Gate", // must always be shown on gates + "Pack", // must always be shown on packable entities + "Training", // lets hope training and construction never happen together + "Construction", + "Research", // normal together with training + + // UNIQUE PANES (importance doesn't matter) + "Command", + "Queue", + "Selection", +]; Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 15374) +++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 15375) @@ -1,1211 +1,1208 @@