Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 13625) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 13626) @@ -1,2099 +1,2123 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; const SDLK_LEFTBRACKET = 91; const SDLK_RIGHTBRACKET = 93; const SDLK_RSHIFT = 303; const SDLK_LSHIFT = 304; const SDLK_RCTRL = 305; const SDLK_LCTRL = 306; const SDLK_RALT = 307; const SDLK_LALT = 308; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too const ACTION_NONE = 0; const ACTION_GARRISON = 1; const ACTION_REPAIR = 2; var preSelectedAction = ACTION_NONE; const INPUT_NORMAL = 0; const INPUT_SELECTING = 1; const INPUT_BANDBOXING = 2; const INPUT_BUILDING_PLACEMENT = 3; const INPUT_BUILDING_CLICK = 4; const INPUT_BUILDING_DRAG = 5; const INPUT_BATCHTRAINING = 6; const INPUT_PRESELECTEDACTION = 7; const INPUT_BUILDING_WALL_CLICK = 8; const INPUT_BUILDING_WALL_PATHING = 9; const INPUT_MASSTRIBUTING = 10; var inputState = INPUT_NORMAL; var placementSupport = new PlacementSupport(); var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; // Number of pixels the mouse can move before the action is considered a drag var maxDragDelta = 4; // Time in milliseconds in which a double click is recognized const doubleClickTime = 500; var doubleClickTimer = 0; var doubleClicked = false; // Store the previously clicked entity - ensure a double/triple click happens on the same entity var prevClickedEntity = 0; // Same double-click behaviour for hotkey presses const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursorAndTooltip() { var cursorSet = false; var tooltipSet = false; var informationTooltip = getGUIObjectByName("informationTooltip"); if (!mouseIsOverObject) { var action = determineAction(mouseX, mouseY); if (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) { if (action) { if (action.cursor) { Engine.SetCursor(action.cursor); cursorSet = true; } if (action.tooltip) { tooltipSet = true; informationTooltip.caption = action.tooltip; informationTooltip.hidden = false; } } } } if (!cursorSet) Engine.SetCursor("arrow-default"); if (!tooltipSet) informationTooltip.hidden = true; var placementTooltip = 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 = result.success ? "" : result.message; - return result.success; + + if (!result.success) + 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 = "Basic range: "+Math.round(cmd.range/4)+"\nAverage bonus 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 (!gatherer || !supply) return undefined; if (gatherer[supply.type.generic+"."+supply.type.specific]) return supply.type.specific; if (gatherer[supply.type.generic]) return supply.type.generic; return undefined; } function getActionInfo(action, target) { var 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 the selection isn't friendly units, no action var playerID = Engine.GetPlayerID(); var allOwnedByPlayer = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.player == playerID; }); if (!g_DevSettings.controlAll && !allOwnedByPlayer) return {"possible": false}; // Work out whether the selection can have rally points var haveRallyPoints = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.rallyPoint; }); if (!target) { if (action == "set-rallypoint" && haveRallyPoints) return {"possible": true}; else if (action == "move" || action == "attack-move") return {"possible": true}; else return {"possible": false}; } if (haveRallyPoints && selection.indexOf(target) != -1 && action == "unset-rallypoint") return {"possible": true}; // Look at the first targeted entity // (TODO: maybe we eventually want to look at more, and be more context-sensitive? // e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse) var targetState = GetEntityState(target); // Look to see what type of command units going to the rally point should use if (haveRallyPoints && action == "set-rallypoint") { // haveRallyPoints ensures all selected entities can have rally points. // We assume that all entities are owned by the same player. var entState = GetEntityState(selection[0]); var playerState = simState.players[entState.player]; var playerOwned = (targetState.player == entState.player); var allyOwned = playerState.isAlly[targetState.player]; var mutualAllyOwned = playerState.isMutualAlly[targetState.player]; var enemyOwned = playerState.isEnemy[targetState.player]; var gaiaOwned = (targetState.player == 0); var cursor = ""; var tooltip; // default to walking there var data = {command: "walk"}; if (targetState.garrisonHolder && (playerOwned || mutualAllyOwned)) { data.command = "garrison"; data.target = target; cursor = "action-garrison"; tooltip = "Current garrison: " + targetState.garrisonHolder.entities.length + "/" + targetState.garrisonHolder.capacity; if (targetState.garrisonHolder.entities.length >= targetState.garrisonHolder.capacity) tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; } else if (targetState.resourceSupply) { var resourceType = targetState.resourceSupply.type; if (resourceType.generic == "treasure") { cursor = "action-gather-" + resourceType.generic; } else { cursor = "action-gather-" + resourceType.specific; } data.command = "gather"; data.resourceType = resourceType; data.resourceTemplate = targetState.template; } else if (targetState.foundation && entState.buildEntities) { data.command = "build"; data.target = target; cursor = "action-build"; } else if (targetState.needsRepair && allyOwned) { data.command = "repair"; data.target = target; cursor = "action-repair"; } else if (hasClass(entState, "Market") && hasClass(targetState, "Market") && entState.id != targetState.id && (!hasClass(entState, "NavalMarket") || hasClass(targetState, "NavalMarket")) && !enemyOwned) { // Find a trader (if any) that this building can produce. var trader; if (entState.production && entState.production.entities.length) for (var i = 0; i < entState.production.entities.length; ++i) if ((trader = GetTemplateData(entState.production.entities[i]).trader)) break; var traderData = { "firstMarket": entState.id, "secondMarket": targetState.id, "template": trader }; var gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData); if (gain && gain.traderGain) { data.command = "trade"; data.target = traderData.secondMarket; data.source = traderData.firstMarket; cursor = "action-setup-trade-route"; tooltip = "Right-click to establish a default route for new traders."; if (trader) tooltip += "\nGain (metal): " + getTradingTooltip(gain); else // Foundation or cannot produce traders tooltip += "\nExpected gain (metal): " + getTradingTooltip(gain); } } // Don't allow the rally point to be set on any of the currently selected entities for (var i = 0; i < selection.length; i++) if (target === selection[i]) return {"possible": false}; return {"possible": true, "data": data, "position": targetState.position, "cursor": cursor, "tooltip": tooltip}; } // 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 = GetEntityState(entityID); if (!entState) continue; var playerState = simState.players[entState.player]; var playerOwned = (targetState.player == entState.player); var allyOwned = playerState.isAlly[targetState.player]; var mutualAllyOwned = playerState.isMutualAlly[targetState.player]; var neutralOwned = playerState.isNeutral[targetState.player]; var enemyOwned = playerState.isEnemy[targetState.player]; var gaiaOwned = (targetState.player == 0); // Find the resource type we're carrying, if any var carriedType = undefined; if (entState.resourceCarrying && entState.resourceCarrying.length) carriedType = entState.resourceCarrying[0].type; switch (action) { case "garrison": if (hasClass(entState, "Unit") && targetState.garrisonHolder && (playerOwned || mutualAllyOwned)) { var tooltip = "Current garrison: " + targetState.garrisonHolder.entities.length + "/" + targetState.garrisonHolder.capacity; if (targetState.garrisonHolder.entities.length >= targetState.garrisonHolder.capacity) tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; var allowedClasses = targetState.garrisonHolder.allowedClasses; for each (var unitClass in entState.identity.classes) { if (allowedClasses.indexOf(unitClass) != -1) { return {"possible": true, "tooltip": tooltip}; } } } break; case "setup-trade-route": // If ground or sea trade possible if (!targetState.foundation && ((entState.trader && hasClass(entState, "Organic") && (playerOwned || allyOwned) && hasClass(targetState, "Market")) || (entState.trader && hasClass(entState, "Ship") && (playerOwned || allyOwned) && hasClass(targetState, "NavalMarket")))) { var tradingData = {"trader": entState.id, "target": target}; var tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", tradingData); var tooltip; if (tradingDetails === null) return {"possible": false}; switch (tradingDetails.type) { case "is first": tooltip = "Origin trade market."; if (tradingDetails.hasBothMarkets) tooltip += "\nGain (" + tradingDetails.goods + "): " + getTradingTooltip(tradingDetails.gain); else tooltip += "\nRight-click on another market to set it as a destination trade market." break; case "is second": tooltip = "Destination trade market.\nGain (" + tradingDetails.goods + "): " + getTradingTooltip(tradingDetails.gain); break; case "set first": tooltip = "Right-click to set as origin trade market"; break; case "set second": tooltip = "Right-click to set as destination trade market.\nGain (" + tradingDetails.goods + "): " + getTradingTooltip(tradingDetails.gain); break; } return {"possible": true, "tooltip": tooltip}; } break; case "heal": // The check if the target is unhealable is done by targetState.needsHeal if (entState.Healer && hasClass(targetState, "Unit") && targetState.needsHeal && (playerOwned || allyOwned)) { // Healers can't heal themselves. if (entState.id == targetState.id) return {"possible": false}; var unhealableClasses = entState.Healer.unhealableClasses; for each (var unitClass in targetState.identity.classes) { if (unhealableClasses.indexOf(unitClass) != -1) { return {"possible": false}; } } var healableClasses = entState.Healer.healableClasses; for each (var unitClass in targetState.identity.classes) { if (healableClasses.indexOf(unitClass) != -1) { return {"possible": true}; } } } break; case "gather": if (targetState.resourceSupply) { var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply); if (resource) return {"possible": true, "cursor": "action-gather-" + resource}; } break; case "returnresource": if (targetState.resourceDropsite && playerOwned && carriedType && targetState.resourceDropsite.types.indexOf(carriedType) != -1) return {"possible": true, "cursor": "action-return-" + carriedType}; break; case "build": if (targetState.foundation && entState.buildEntities && playerOwned) return {"possible": true}; break; case "repair": if (entState.buildEntities && targetState.needsRepair && allyOwned) return {"possible": true}; break; case "attack": if (entState.attack && targetState.hitpoints && (enemyOwned || neutralOwned)) return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": target})}; break; } } if (action == "move" || action == "attack-move") return {"possible": true}; else return {"possible": false}; } /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ function determineAction(x, y, fromMinimap) { var selection = g_Selection.toList(); // No action if there's no selection if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return undefined; // If the selection isn't friendly units, no action var playerID = Engine.GetPlayerID(); var allOwnedByPlayer = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.player == playerID; }); if (!g_DevSettings.controlAll && !allOwnedByPlayer) return undefined; // Work out whether the selection can have rally points var haveRallyPoints = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.rallyPoint; }); var targets = []; var target = undefined; var type = "none"; var cursor = ""; var targetState = undefined; if (!fromMinimap) targets = Engine.PickEntitiesAtPoint(x, y); if (targets.length) { target = targets[0]; } var actionInfo = undefined; if (preSelectedAction != ACTION_NONE) { switch (preSelectedAction) { case ACTION_GARRISON: if ((actionInfo = getActionInfo("garrison", target)).possible) return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target}; else return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined}; break; case ACTION_REPAIR: if (getActionInfo("repair", target).possible) return {"type": "repair", "cursor": "action-repair", "target": target}; else return {"type": "none", "cursor": "action-repair-disabled", "target": undefined}; break; } } else if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible) { return {"type": "attack", "cursor": "action-attack", "target": target}; } else if (Engine.HotkeyIsPressed("session.garrison") && (actionInfo = getActionInfo("garrison", target)).possible) { return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target}; } else if (Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible) { return {"type": "attack-move", "cursor": "action-attack-move"}; } else { if ((actionInfo = getActionInfo("setup-trade-route", target)).possible) return {"type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target}; else if ((actionInfo = getActionInfo("gather", target)).possible) return {"type": "gather", "cursor": actionInfo.cursor, "target": target}; else if ((actionInfo = getActionInfo("returnresource", target)).possible) return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target}; else if (getActionInfo("build", target).possible) return {"type": "build", "cursor": "action-build", "target": target}; else if (getActionInfo("repair", target).possible) return {"type": "build", "cursor": "action-repair", "target": target}; else if ((actionInfo = getActionInfo("set-rallypoint", target)).possible) return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position}; else if (getActionInfo("heal", target).possible) return {"type": "heal", "cursor": "action-heal", "target": target}; else if (getActionInfo("attack", target).possible) return {"type": "attack", "cursor": "action-attack", "target": target}; else if (getActionInfo("unset-rallypoint", target).possible) return {"type": "unset-rallypoint"}; else if (getActionInfo("move", target).possible) return {"type": "move"}; } return {"type": type, "cursor": cursor, "target": target}; } var dragStart; // used for remembering mouse coordinates at start of drag operations function tryPlaceBuilding(queued) { if (placementSupport.mode !== "building") { error("[tryPlaceBuilding] Called while in '"+placementSupport.mode+"' placement mode instead of 'building'"); return false; } var selection = g_Selection.toList(); // Use the preview to check it's a valid build location if (!updateBuildingPlacementPreview()) { // invalid location - don't build it // TODO: play a sound? return false; } // Start the construction Engine.PostNetworkCommand({ "type": "construct", "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "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("[tryPlaceWall] Called while in '" + placementSupport.mode + "' placement mode; expected 'wall' mode"); return false; } var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object")) { error("[tryPlaceWall] Unexpected return value from updateBuildingPlacementPreview: '" + uneval(placementInfo) + "'; expected either 'false' or 'object'"); return false; } if (!wallPlacementInfo) return false; var selection = g_Selection.toList(); var cmd = { "type": "construct-wall", "autorepair": true, "autocontinue": true, "queued": queued, "entities": selection, "wallSet": placementSupport.wallSet, "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, }; // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed // (this is somewhat non-ideal and hardcode-ish) var hasWallSegment = false; for (var k in cmd.pieces) { if (cmd.pieces[k].template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( { hasWallSegment = true; break; } } if (hasWallSegment) { Engine.PostNetworkCommand(cmd); Engine.GuiInterfaceCall("PlaySound", {"name": "order_repair", "entity": selection[0] }); } return true; } // Limits bandboxed selections to certain types of entities based on priority function getPreferredEntities(ents) { var entStateList = []; var preferredEnts = []; // Check if there are units in the selection and get a list of entity states for each (var ent in ents) { var entState = GetEntityState(ent); if (!entState) continue; if (hasClass(entState, "Unit")) preferredEnts.push(ent); entStateList.push(entState); } // If there are no units, check if there are defensive entities in the selection if (!preferredEnts.length) for (var i = 0; i < ents.length; i++) if (hasClass(entStateList[i], "Defensive")) preferredEnts.push(ents[i]); return preferredEnts; } // Removes any support units from the passed list of entities function getMilitaryEntities(ents) { var militaryEnts = []; for each (var ent in ents) { var entState = GetEntityState(ent); if (!hasClass(entState, "Support")) militaryEnts.push(ent); } return militaryEnts; } function handleInputBeforeGui(ev, hoveredObject) { // Capture mouse position so we can use it for displaying cursors, // and key states switch (ev.type) { case "mousebuttonup": case "mousebuttondown": case "mousemotion": mouseX = ev.x; mouseY = ev.y; break; } // Remember whether the mouse is over a GUI object or not mouseIsOverObject = (hoveredObject != null); // Close the menu when interacting with the game world if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) closeMenu(); // State-machine processing: // // (This is for states which should override the normal GUI processing - events will // be processed here before being passed on, and propagation will stop if this function // returns true) // // TODO: it'd probably be nice to have a better state-machine system, with guaranteed // entry/exit functions, since this is a bit broken now switch (inputState) { case INPUT_BANDBOXING: switch (ev.type) { case "mousemotion": var x0 = dragStart[0]; var y0 = dragStart[1]; var x1 = ev.x; var y1 = ev.y; if (x0 > x1) { var t = x0; x0 = x1; x1 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; } var bandbox = getGUIObjectByName("bandbox"); bandbox.size = [x0, y0, x1, y1].join(" "); bandbox.hidden = false; // TODO: Should we handle "control all units" here as well? var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID()); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var x0 = dragStart[0]; var y0 = dragStart[1]; var x1 = ev.x; var y1 = ev.y; if (x0 > x1) { var t = x0; x0 = x1; x1 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; } var bandbox = getGUIObjectByName("bandbox"); bandbox.hidden = true; // Get list of entities limited to preferred entities // TODO: Should we handle "control all units" here as well? var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID()); var preferredEntities = getPreferredEntities(ents) if (preferredEntities.length) { ents = preferredEntities; if (Engine.HotkeyIsPressed("selection.milonly")) { var militaryEntities = getMilitaryEntities(ents); if (militaryEntities.length) ents = militaryEntities; } } // Remove the bandbox hover highlighting g_Selection.setHighlightList([]); // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel selection var bandbox = getGUIObjectByName("bandbox"); bandbox.hidden = true; g_Selection.setHighlightList([]); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_CLICK: switch (ev.type) { case "mousemotion": // If the mouse moved far enough from the original click location, // then switch to drag-orientation mode var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; var maxDragDelta = 16; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { inputState = INPUT_BUILDING_DRAG; return false; } break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_CLICK: // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. switch (ev.type) { case "mousebuttonup": if (ev.button === SDL_BUTTON_LEFT) { inputState = INPUT_BUILDING_WALL_PATHING; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_PATHING: // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint. // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the // user to continue building walls. switch (ev.type) { case "mousemotion": placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!) // the ending point and the starting point to snap to. // // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers // in them. Might be useful to query only for entities within a certain range around the starting point and ending // points. placementSupport.wallSnapEntitiesIncludeOffscreen = true; var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates if (result && result.cost) { placementSupport.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 = "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) { // Handle the time-warp testing features, restricted to single-player if (!g_IsNetworked && getGUIObjectByName("devTimeWarp").checked) { if (ev.type == "hotkeydown" && ev.hotkey == "timewarp.fastforward") Engine.SetSimRate(20.0); else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.fastforward") Engine.SetSimRate(1.0); else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.rewind") Engine.RewindTimeWarp(); } if (ev.hotkey == "session.showstatusbars") { g_ShowAllStatusBars = (ev.type == "hotkeydown"); recalculateStatusBarDisplay(); } // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": // Highlight the first hovered entity (if any) var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (ents.length) g_Selection.setHighlightList([ents[0]]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { dragStart = [ ev.x, ev.y ]; inputState = INPUT_SELECTING; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { var action = determineAction(ev.x, ev.y); if (!action) break; return doAction(action, ev); } break; case "hotkeydown": if (ev.hotkey.indexOf("selection.group.") == 0) { var now = new Date(); if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey)) { if (ev.hotkey.indexOf("selection.group.select.") == 0) { var sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { var sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now.getTime(); prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) { var action = determineAction(ev.x, ev.y); if (!action) break; preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; return doAction(action, ev); } else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } // else default: // Slight hack: If selection is empty, reset the input state if (g_Selection.toList().length == 0) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": // If the mouse moved further than a limit, switch to bandbox mode var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { inputState = INPUT_BANDBOXING; return false; } var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (!ents.length) { if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) { g_Selection.reset(); resetIdleUnit(); } inputState = INPUT_NORMAL; return true; } var selectedEntity = ents[0]; var now = new Date(); // If camera following and we select different unit, stop if (Engine.GetFollowedEntity() != selectedEntity) { Engine.CameraFollow(0); } if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity)) { // Double click or triple click has occurred var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); var matchRank = true; var templateToMatch; // Check for double click or triple click if (!doubleClicked) { // If double click hasn't already occurred, this is a double click. // Select similar units regardless of rank templateToMatch = 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"); switch (action.type) { case "move": var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "attack-move": var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "attack": Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); return true; case "heal": Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] }); return true; case "build": // (same command as repair) case "repair": Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); return true; case "gather": Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; case "returnresource": Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; case "setup-trade-route": Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] }); return true; case "garrison": Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] }); return true; case "set-rallypoint": var pos = undefined; // if there is a position set in the action then use this so that when setting a // rally point on an entity it is centered on that entity if (action.position) { pos = action.position; } else { pos = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); } Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": pos.x, "z": pos.z, "data": action.data, "queued": queued}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": pos.x, "z": pos.z, "queued": queued }); return true; case "unset-rallypoint": var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection}); // Remove displayed rally point Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": [] }); return true; case "none": return true; default: error("Invalid action.type "+action.type); return false; } } function handleMinimapEvent(target) { // Partly duplicated from handleInputAfterGui(), but with the input being // world coordinates instead of screen coordinates. if (inputState == INPUT_NORMAL) { var fromMinimap = true; var action = determineAction(undefined, undefined, fromMinimap); if (!action) return false; var selection = g_Selection.toList(); var queued = Engine.HotkeyIsPressed("session.queue"); switch (action.type) { case "move": Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "attack-move": Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "set-rallypoint": Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z, "queued": queued }); return true; default: error("Invalid action.type "+action.type); } } return false; } // Called by GUI when user clicks construction button // @param buildTemplate Template name of the entity the user wants to build function startBuildingPlacement(buildTemplate, playerState) { if(getEntityLimitAndCount(playerState, buildTemplate)[2] == 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 preferred trading goods function selectTradingPreferredGoods(data) { Engine.PostNetworkCommand({"type": "select-trading-goods", "entities": data.entities, "preferredGoods": data.preferredGoods}); } // Called by GUI when user clicks exchange resources button function exchangeResources(command) { Engine.PostNetworkCommand({"type": "barter", "sell": command.sell, "buy": command.buy, "amount": command.amount}); } // 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 = g_ConfigDB.system["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 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 canBeAddedCount = undefined; if (entCategory && playerState.entityLimits[entCategory]) { entLimit = playerState.entityLimits[entCategory]; entCount = playerState.entityCounts[entCategory]; canBeAddedCount = Math.max(entLimit - entCount, 0); } return [entLimit, entCount, canBeAddedCount]; } // 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 = getAllTrainableEntities(selection); // Check if the position is valid if (!trainableEnts.length || trainableEnts.length <= position) return; var entToTrain = trainableEnts[position]; addTrainingToQueue(selection, entToTrain, playerState); return; } // Called by GUI when user clicks training button function addTrainingToQueue(selection, trainEntType, playerState) { // Create list of buildings which can train trainEntType var appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); // Check trainEntType entity limit and count var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType) // Batch training possible if we can train at least 2 units var batchTrainingPossible = canBeTrainedCount == undefined || canBeTrainedCount > 1; 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) { if (Engine.GuiInterfaceCall("GetNeededResources", multiplyEntityCosts( template, batchTrainingCount + batchIncrementSize))) return; batchTrainingCount += batchIncrementSize; } batchTrainingEntityAllowedCount = canBeTrainedCount; 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; batchTrainingCount = batchIncrementSize; } else { // Non-batched - just create a single entity in each building // (but no more than entity limit allows) var buildingsForTraining = appropriateBuildings; if (trainEntLimit) buildingsForTraining = buildingsForTraining.slice(0, canBeTrainedCount); Engine.PostNetworkCommand({"type": "train", "template": trainEntType, "count": 1, "entities": buildingsForTraining}); } } // Called by GUI when user clicks research button function addResearchToQueue(entity, researchType) { Engine.PostNetworkCommand({"type": "research", "entity": entity, "template": researchType}); } // Returns the number of units that will be present in a batch if the user clicks // the training button with shift down function getTrainingBatchStatus(playerState, entity, trainEntType, selection) { var appropriateBuildings = [entity]; if (selection && selection.indexOf(entity) != -1) appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); var nextBatchTrainingCount = 0; var currentBatchTrainingCount = 0; if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 && batchTrainingType == trainEntType) { nextBatchTrainingCount = batchTrainingCount; currentBatchTrainingCount = batchTrainingCount; var canBeTrainedCount = batchTrainingEntityAllowedCount; } else { var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType); var batchSize = Math.min(canBeTrainedCount, batchIncrementSize); } // We need to calculate count after the next increment if it's possible if (canBeTrainedCount == undefined || canBeTrainedCount > nextBatchTrainingCount * appropriateBuildings.length) nextBatchTrainingCount += batchIncrementSize; // If training limits don't allow us to train batchTrainingCount in each appropriate building // train as many full batches as we can and remainer in one more building. var buildingsCountToTrainFullBatch = appropriateBuildings.length; var remainderToTrain = 0; if (canBeTrainedCount !== undefined && canBeTrainedCount < nextBatchTrainingCount * appropriateBuildings.length) { buildingsCountToTrainFullBatch = Math.floor(canBeTrainedCount / nextBatchTrainingCount); remainderToTrain = canBeTrainedCount % nextBatchTrainingCount; } return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain, 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) { var entState = GetEntityState(entity); var template = GetTemplateData(entState.template); var unitName = getEntityName(template); var playerID = Engine.GetPlayerID(); var simState = GetSimState(); if (entState.player == playerID || g_DevSettings.controlAll) { switch (commandName) { case "delete": var selection = g_Selection.toList(); if (selection.length > 0) if (!entState.resourceSupply || !entState.resourceSupply.killBeforeGather) openDeleteDialog(selection); break; case "stop": var selection = g_Selection.toList(); if (selection.length > 0) stopUnits(selection); break; case "garrison": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; break; case "repair": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; break; case "unload-all": unloadAll(); break; case "focus-rally": // if the selected building has a rally point set, move the camera to it; otherwise, move to the building itself // (since that's where units will spawn without a rally point) var focusTarget = null; if (entState.rallyPoint && entState.rallyPoint.position) { focusTarget = entState.rallyPoint.position; } else { if (entState.position) focusTarget = entState.position; } if (focusTarget !== null) Engine.CameraMoveTo(focusTarget.x, focusTarget.z); break; default: break; } } else if (simState.players[playerID].isMutualAlly[entState.player]) { switch (commandName) { case "garrison": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; break; default: break; } } } } // Performs the specified formation function performFormation(entity, formationName) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "formation", "entities": selection, "name": formationName }); } } // Performs the specified group function performGroup(action, groupId) { switch (action) { case "snap": case "select": case "add": var toSelect = []; g_Groups.update(); for (var ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); if (action != "add") g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) Engine.CameraFollow(toSelect[0]); break; case "save": var selection = g_Selection.toList(); g_Groups.groups[groupId].reset(); g_Groups.addEntities(groupId, selection); updateGroups(); break; } } // Performs the specified stance function performStance(entity, stanceName) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "stance", "entities": selection, "name": stanceName }); } } // Lock / Unlock the gate function lockGate(lock) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "lock-gate", "entities": selection, "lock": lock, }); } // Pack / unpack unit(s) function packUnit(pack) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "pack", "entities": selection, "pack": pack, "queued": false }); } // Cancel un/packing unit(s) function cancelPackUnit(pack) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "cancel-pack", "entities": selection, "pack": pack, "queued": false }); } // Transform a wall to a gate function transformWallToGate(template) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "wall-to-gate", "entities": selection.filter( function(e) { return getWallGateTemplate(e) == template } ), "template": template, }); } // Gets the gate form (if any) of a given long wall piece function getWallGateTemplate(entity) { // TODO: find the gate template name in a better way var entState = GetEntityState(entity); var index; if (entState && !entState.foundation && hasClass(entState, "LongWall") && (index = entState.template.indexOf("long")) >= 0) return entState.template.substr(0, index) + "gate"; return undefined; } // Set the camera to follow the given unit function setCameraFollow(entity) { // Follow the given entity if it's a unit if (entity) { var entState = GetEntityState(entity); if (entState && hasClass(entState, "Unit")) { Engine.CameraFollow(entity); return; } } // Otherwise stop following Engine.CameraFollow(0); } var lastIdleUnit = 0; var currIdleClass = 0; var lastIdleType = undefined; function resetIdleUnit() { lastIdleUnit = 0; currIdleClass = 0; lastIdleType = undefined; } function findIdleUnit(classes) { var append = Engine.HotkeyIsPressed("selection.add"); var selectall = Engine.HotkeyIsPressed("selection.offscreen"); // Reset the last idle unit, etc., if the selection type has changed. var type = classes.join(); if (selectall || type != lastIdleType) resetIdleUnit(); lastIdleType = type; // If selectall is true, there is no limit and it's necessary to iterate // over all of the classes, resetting only when the first match is found. var matched = false; for (var i = 0; i < classes.length; ++i) { var data = { idleClass: classes[currIdleClass], prevUnit: lastIdleUnit, limit: 1 }; if (append) data.excludeUnits = g_Selection.toList(); if (selectall) data = { idleClass: classes[currIdleClass] }; // Check if we have new valid entity var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); if (idleUnits.length && idleUnits[0] != lastIdleUnit) { lastIdleUnit = idleUnits[0]; if (!append && (!selectall || selectall && !matched)) g_Selection.reset() if (selectall) g_Selection.addList(idleUnits); else { g_Selection.addList([lastIdleUnit]); var position = GetEntityState(lastIdleUnit).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 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 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/placement.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/placement.js (revision 13625) +++ ps/trunk/binaries/data/mods/public/gui/session/placement.js (revision 13626) @@ -1,38 +1,40 @@ function PlacementSupport() { this.Reset(); } PlacementSupport.DEFAULT_ANGLE = Math.PI*3/4; /** * Resets the building placement support state. Use this to cancel construction of an entity. */ PlacementSupport.prototype.Reset = function() { this.mode = null; this.position = null; this.template = null; this.tooltipMessage = ""; // tooltip text to show while the user is placing a building this.tooltipError = false; this.wallSet = null; // maps types of wall pieces ("tower", "long", "short", ...) to template names this.wallSnapEntities = null; // list of candidate entities to snap the starting and (!) ending positions to when building walls this.wallEndPosition = null; this.wallSnapEntitiesIncludeOffscreen = false; // should the next update of the snap candidate list include offscreen towers? this.SetDefaultAngle(); this.RandomizeActorSeed(); + + this.attack = null; Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""}); Engine.GuiInterfaceCall("SetWallPlacementPreview", {"wallSet": null}); }; PlacementSupport.prototype.SetDefaultAngle = function() { this.angle = PlacementSupport.DEFAULT_ANGLE; }; PlacementSupport.prototype.RandomizeActorSeed = function() { this.actorSeed = Math.floor(65535 * Math.random()); }; Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 13625) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 13626) @@ -1,329 +1,347 @@ function layoutSelectionSingle() { getGUIObjectByName("detailsAreaSingle").hidden = false; getGUIObjectByName("detailsAreaMultiple").hidden = true; } function layoutSelectionMultiple() { getGUIObjectByName("detailsAreaMultiple").hidden = false; getGUIObjectByName("detailsAreaSingle").hidden = true; } // 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 += " -- Packed"; 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 = "[OFFLINE] " + playerName; } // Rank if (entState.identity && entState.identity.rank && entState.identity.classes) { getGUIObjectByName("rankIcon").tooltip = entState.identity.rank + " Rank"; getGUIObjectByName("rankIcon").sprite = getRankIconSprite(entState); getGUIObjectByName("rankIcon").hidden = false; } else { getGUIObjectByName("rankIcon").hidden = true; getGUIObjectByName("rankIcon").tooltip = ""; } // Hitpoints if (entState.hitpoints) { var unitHealthBar = getGUIObjectByName("healthBar"); var healthSize = unitHealthBar.size; healthSize.rright = 100*Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints)); unitHealthBar.size = healthSize; var hitpoints = Math.ceil(entState.hitpoints) + " / " + entState.maxHitpoints; getGUIObjectByName("healthStats").caption = hitpoints; getGUIObjectByName("healthSection").hidden = false; } else { getGUIObjectByName("healthSection").hidden = true; } // TODO: Stamina var player = Engine.GetPlayerID(); if (entState.stamina && (entState.player == player || g_DevSettings.controlAll)) { getGUIObjectByName("staminaSection").hidden = false; } else { getGUIObjectByName("staminaSection").hidden = true; } // Experience if (entState.promotion) { var experienceBar = 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; var experience = "[font=\"serif-bold-13\"]Experience: [/font]" + Math.floor(entState.promotion.curr); if (entState.promotion.curr < entState.promotion.req) experience += " / " + entState.promotion.req; getGUIObjectByName("experience").tooltip = experience; getGUIObjectByName("experience").hidden = false; } else { getGUIObjectByName("experience").hidden = true; } // Resource stats if (entState.resourceSupply) { var resources = entState.resourceSupply.isInfinite ? "\u221E" : // Infinity symbol Math.ceil(+entState.resourceSupply.amount) + " / " + entState.resourceSupply.max; var resourceType = entState.resourceSupply.type["generic"]; if (resourceType == "treasure") resourceType = entState.resourceSupply.type["specific"]; var unitResourceBar = 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; getGUIObjectByName("resourceLabel").caption = toTitleCase(resourceType) + ":"; getGUIObjectByName("resourceStats").caption = resources; if (entState.hitpoints) getGUIObjectByName("resourceSection").size = getGUIObjectByName("staminaSection").size; else getGUIObjectByName("resourceSection").size = getGUIObjectByName("healthSection").size; getGUIObjectByName("resourceSection").hidden = false; } else { 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]; getGUIObjectByName("resourceCarryingIcon").hidden = false; getGUIObjectByName("resourceCarryingText").hidden = false; getGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+carried.type+".png"; getGUIObjectByName("resourceCarryingText").caption = carried.amount + " / " + carried.max; getGUIObjectByName("resourceCarryingIcon").tooltip = ""; } // Use the same indicators for traders else if (entState.trader && entState.trader.goods.amount) { getGUIObjectByName("resourceCarryingIcon").hidden = false; getGUIObjectByName("resourceCarryingText").hidden = false; 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; getGUIObjectByName("resourceCarryingText").caption = totalGain; getGUIObjectByName("resourceCarryingIcon").tooltip = "Gain: " + getTradingTooltip(entState.trader.goods.amount); } // And for number of workers else if (entState.foundation) { getGUIObjectByName("resourceCarryingIcon").hidden = false; getGUIObjectByName("resourceCarryingText").hidden = false; getGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; getGUIObjectByName("resourceCarryingText").caption = entState.foundation.numBuilders + " "; getGUIObjectByName("resourceCarryingIcon").tooltip = "Number of builders"; } else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints)) { getGUIObjectByName("resourceCarryingIcon").hidden = false; getGUIObjectByName("resourceCarryingText").hidden = false; getGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; getGUIObjectByName("resourceCarryingText").caption = entState.resourceSupply.gatherers.length + " / " + entState.resourceSupply.maxGatherers + " "; getGUIObjectByName("resourceCarryingIcon").tooltip = "Current/max gatherers"; } else { getGUIObjectByName("resourceCarryingIcon").hidden = true; getGUIObjectByName("resourceCarryingText").hidden = true; } // Set Player details getGUIObjectByName("specific").caption = specificName; getGUIObjectByName("player").caption = playerName; getGUIObjectByName("playerColorBackground").sprite = "colour: " + playerColor; if (genericName) { getGUIObjectByName("generic").caption = "(" + genericName + ")"; } else { getGUIObjectByName("generic").caption = ""; } if ("Gaia" != civName) { getGUIObjectByName("playerCivIcon").sprite = "stretched:grayscale:" + civEmblem; getGUIObjectByName("player").tooltip = civName; } else { getGUIObjectByName("playerCivIcon").sprite = ""; getGUIObjectByName("player").tooltip = ""; } // Icon image if (template.icon) { getGUIObjectByName("icon").sprite = "stretched:session/portraits/" + template.icon; } else { // TODO: we should require all entities to have icons, so this case never occurs - getGUIObjectByName("icon").sprite = "bkFillBlack"; - } - - // Attack and Armor - var type = ""; - if (entState.attack) - type = entState.attack.type + " "; - - attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack); - // Show max attack range if ranged attack, also convert to tiles (4m per tile) - if (entState.attack && entState.attack.type == "Ranged") - attack += ", [font=\"serif-bold-13\"]Range:[/font] " + Math.round(entState.attack.maxRange/4); - getGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n[font=\"serif-bold-13\"]Armor:[/font] " + armorTypeDetails(entState.armour); - - // Icon Tooltip - var iconTooltip = ""; - - if (genericName) + getGUIObjectByName("icon").sprite = "bkFillBlack"; + } + + // Attack and Armor + var type = ""; + var attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack); + if (entState.attack) + { + type = entState.attack.type + " "; + + // Show max attack range if ranged attack, also convert to tiles (4m per tile) + if (entState.attack.type == "Ranged") + { + var realRange = entState.attack.elevationAdaptedRange; + var range = entState.attack.maxRange; + attack += ", [font=\"serif-bold-13\"]Range:[/font] " + + Math.round(range/4); + + if (Math.round((realRange - range)/4) > 0) + { + attack += " (+" + Math.round((realRange - range)/4) + ")"; + } + else if (Math.round((realRange - range)/4) < 0) + { + attack += " (" + Math.round((realRange - range)/4) + ")"; + } // don't show when it's 0 + + } + } + + getGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n[font=\"serif-bold-13\"]Armor:[/font] " + armorTypeDetails(entState.armour); + + // Icon Tooltip + var iconTooltip = ""; + + if (genericName) iconTooltip = "[font=\"serif-bold-16\"]" + genericName + "[/font]"; if (template.tooltip) iconTooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]"; getGUIObjectByName("iconBorder").tooltip = iconTooltip; // Unhide Details Area getGUIObjectByName("detailsAreaSingle").hidden = false; 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 = getGUIObjectByName("healthBarMultiple"); var healthSize = unitHealthBar.size; healthSize.rtop = 100-100*Math.max(0, Math.min(1, averageHealth / maxHealth)); unitHealthBar.size = healthSize; var hitpoints = "[font=\"serif-bold-13\"]Hitpoints [/font]" + averageHealth + " / " + maxHealth; var healthMultiple = getGUIObjectByName("healthMultiple"); healthMultiple.tooltip = hitpoints; healthMultiple.hidden = false; } else { getGUIObjectByName("healthMultiple").hidden = true; } // TODO: Stamina // getGUIObjectByName("staminaBarMultiple"); getGUIObjectByName("numberOfUnits").caption = selection.length; // Unhide Details Area getGUIObjectByName("detailsAreaMultiple").hidden = false; getGUIObjectByName("detailsAreaSingle").hidden = true; } // Updates middle entity Selection Details Panel function updateSelectionDetails() { var supplementalDetailsPanel = getGUIObjectByName("supplementalSelectionDetails"); var detailsPanel = getGUIObjectByName("selectionDetails"); var commandsPanel = getGUIObjectByName("unitCommands"); g_Selection.update(); var selection = g_Selection.toList(); if (selection.length == 0) { getGUIObjectByName("detailsAreaMultiple").hidden = true; 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 = GetEntityState(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 Panels supplementalDetailsPanel.hidden = false; detailsPanel.hidden = false; commandsPanel.hidden = false; // Fill out commands panel for specific unit selected (or first unit of primary group) updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection); } Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 13625) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 13626) @@ -1,714 +1,723 @@ function Attack() {} var bonusesSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; var preferredClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; var restrictedClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + "" + "" + "10.0" + "0.0" + "5.0" + "4.0" + "1000" + "" + "" + "pers" + "Infantry" + "1.5" + "" + "" + "Cavalry Melee" + "1.5" + "" + "" + "Champion" + "Cavalry Infantry" + "" + "" + "0.0" + "10.0" + "0.0" + "44.0" + "20.0" + + ""+ + "" + + "" + "800" + "1600" + "50.0" + "2.5" + "" + "" + "Cavalry" + "2" + "" + "" + "Champion" + "" + "Circular" + "20" + "false" + "0.0" + "10.0" + "0.0" + "" + "" + "" + "10.0" + "0.0" + "50.0" + "24.0" + "20.0" + "" + "" + "1000.0" + "0.0" + "0.0" + "4.0" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + bonusesSchema + preferredClassesSchema + restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + + ""+ + "" + + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + bonusesSchema + preferredClassesSchema + restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + bonusesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? "" + bonusesSchema + preferredClassesSchema + restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? bonusesSchema + preferredClassesSchema + restrictedClassesSchema + "" + "" + ""; Attack.prototype.Init = function() { }; Attack.prototype.Serialize = null; // we have no dynamic state to save Attack.prototype.GetAttackTypes = function() { var ret = []; if (this.template.Charge) ret.push("Charge"); if (this.template.Melee) ret.push("Melee"); if (this.template.Ranged) ret.push("Ranged"); return ret; }; Attack.prototype.GetPreferredClasses = function(type) { if (this.template[type] && this.template[type].PreferredClasses && this.template[type].PreferredClasses._string) { return this.template[type].PreferredClasses._string.split(/\s+/); } return []; }; Attack.prototype.GetRestrictedClasses = function(type) { if (this.template[type] && this.template[type].RestrictedClasses && this.template[type].RestrictedClasses._string) { return this.template[type].RestrictedClasses._string.split(/\s+/); } return []; }; Attack.prototype.CanAttack = function(target) { const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); for each (var type in this.GetAttackTypes()) { var canAttack = true; var restrictedClasses = this.GetRestrictedClasses(type); for each (var targetClass in targetClasses) { if (restrictedClasses.indexOf(targetClass) != -1) { canAttack = false; break; } } if (canAttack) { return true; } } return false; }; /** * Returns null if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); var minPref = null; for each (var type in this.GetAttackTypes()) { for each (var targetClass in targetClasses) { var pref = this.GetPreferredClasses(type).indexOf(targetClass); if (pref != -1 && (minPref === null || minPref > pref)) { minPref = pref; } } } return minPref; }; /** * Return the type of the best attack. * TODO: this should probably depend on range, target, etc, * so we can automatically switch between ranged and melee */ Attack.prototype.GetBestAttack = function() { return this.GetAttackTypes().pop(); }; Attack.prototype.GetBestAttackAgainst = function(target) { const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); const isTargetClass = function (value, i, a) { return targetClasses.indexOf(value) != -1; }; const types = this.GetAttackTypes(); const attack = this; const isAllowed = function (value, i, a) { return !attack.GetRestrictedClasses(value).some(isTargetClass); } const isPreferred = function (value, i, a) { return attack.GetPreferredClasses(value).some(isTargetClass); } const byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); } // Always slaughter domestic animals instead of using a normal attack if (isTargetClass("Domestic") && this.template.Slaughter) return "Slaughter"; return types.filter(isAllowed).sort(byPreference).pop(); }; Attack.prototype.CompareEntitiesByPreference = function(a, b) { var aPreference = this.GetPreference(a); var bPreference = this.GetPreference(b); if (aPreference === null && bPreference === null) return 0; if (aPreference === null) return 1; if (bPreference === null) return -1; return aPreference - bPreference; }; Attack.prototype.GetTimers = function(type) { var prepare = +(this.template[type].PrepareTime || 0); prepare = ApplyTechModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity); var repeat = +(this.template[type].RepeatTime || 1000); repeat = ApplyTechModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity); return { "prepare": prepare, "repeat": repeat, "recharge": repeat - prepare }; }; Attack.prototype.GetAttackStrengths = function(type) { // Work out the attack values with technology effects var self = this; var template = this.template[type]; var splash = ""; if (!template) { template = this.template[type.split(".")[0]].Splash; splash = "/Splash"; } var applyTechs = function(damageType) { // All causes caching problems so disable it for now. //var allComponent = ApplyTechModificationsToEntity("Attack/" + type + splash + "/All", +(template[damageType] || 0), self.entity) - self.template[type][damageType]; return ApplyTechModificationsToEntity("Attack/" + type + splash + "/" + damageType, +(template[damageType] || 0), self.entity); }; return { hack: applyTechs("Hack"), pierce: applyTechs("Pierce"), crush: applyTechs("Crush") }; }; Attack.prototype.GetRange = function(type) { var max = +this.template[type].MaxRange; max = ApplyTechModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity); var min = +(this.template[type].MinRange || 0); min = ApplyTechModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); + + var elevationBonus = +(this.template[type].ElevationBonus || 0); + elevationBonus = ApplyTechModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); - return { "max": max, "min": min }; + return { "max": max, "min": min, "elevationBonus": elevationBonus}; }; // Calculate the attack damage multiplier against a target Attack.prototype.GetAttackBonus = function(type, target) { var attackBonus = 1; var template = this.template[type]; if (!template) template = this.template[type.split(".")[0]].Splash; if (template.Bonuses) { var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return 1; // Multiply the bonuses for all matching classes for (var key in template.Bonuses) { var bonus = template.Bonuses[key]; var hasClasses = true; if (bonus.Classes){ var classes = bonus.Classes.split(/\s+/); for (var key in classes) hasClasses = hasClasses && cmpIdentity.HasClass(classes[key]); } if (hasClasses && (!bonus.Civ || bonus.Civ === cmpIdentity.GetCiv())) attackBonus *= bonus.Multiplier; } } return attackBonus; }; // Returns a 2d random distribution scaled for a spread of scale 1. // The current implementation is a 2d gaussian with sigma = 1 Attack.prototype.GetNormalDistribution = function(){ // Use the Box-Muller transform to get a gaussian distribution var a = Math.random(); var b = Math.random(); var c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b); var d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b); return [c, d]; }; /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ Attack.prototype.PerformAttack = function(type, target) { // If this is a ranged attack, then launch a projectile if (type == "Ranged") { // In the future this could be extended: // * Obstacles like trees could reduce the probability of the target being hit // * Obstacles like walls should block projectiles entirely // Get some data about the entity var horizSpeed = +this.template[type].ProjectileSpeed; var gravity = 9.81; // this affects the shape of the curve; assume it's constant for now var spread = this.template.Ranged.Spread; spread = ApplyTechModificationsToEntity("Attack/Ranged/Spread", spread, this.entity); //horizSpeed /= 2; gravity /= 2; // slow it down for testing var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var selfPosition = cmpPosition.GetPosition(); var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; var targetPosition = cmpTargetPosition.GetPosition(); var relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z} var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength} // the component of the targets velocity radially away from the archer var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition); var horizDistance = this.VectorDistance(targetPosition, selfPosition); // This is an approximation of the time ot the target, it assumes that the target has a constant radial // velocity, but since units move in straight lines this is not true. The exact value would be more // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was // about 5% of the units radius out in the worst case) var timeToTarget = horizDistance / (horizSpeed - radialSpeed); // Predict where the unit is when the missile lands. var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget, "z": targetPosition.z + targetVelocity.z * timeToTarget}; // Compute the real target point (based on spread and target speed) var randNorm = this.GetNormalDistribution(); var offsetX = randNorm[0] * spread * (1 + this.VectorLength(targetVelocity) / 20); var offsetZ = randNorm[1] * spread * (1 + this.VectorLength(targetVelocity) / 20); var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ }; // Calculate when the missile will hit the target position var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition); var timeToTarget = realHorizDistance / horizSpeed; var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance}; // Make the arrow appear to land slightly behind the target so that arrows landing next to a guys foot don't count but arrows that go through the torso do var graphicalPosition = {"x": realTargetPosition.x + 2*missileDirection.x, "y": realTargetPosition.y + 2*missileDirection.y}; // Launch the graphical projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); var id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id}); } else { // Melee attack - hurt the target immediately this.CauseDamage({"type": type, "target": target}); } // TODO: charge attacks (need to design how they work) }; /** * Called when some units kills something (another unit, building, animal etc) */ Attack.prototype.TargetKilled = function(killerEntity, targetEntity) { var cmpKillerPlayerStatisticsTracker = QueryOwnerInterface(killerEntity, IID_StatisticsTracker); if (cmpKillerPlayerStatisticsTracker) cmpKillerPlayerStatisticsTracker.KilledEntity(targetEntity); var cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(targetEntity, IID_StatisticsTracker); if (cmpTargetPlayerStatisticsTracker) cmpTargetPlayerStatisticsTracker.LostEntity(targetEntity); // if unit can collect loot, lets try to collect it var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter); if (cmpLooter) { cmpLooter.Collect(targetEntity); } }; Attack.prototype.InterpolatedLocation = function(ent, lateness) { var cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly return undefined; var curPos = cmpTargetPosition.GetPosition(); var prevPos = cmpTargetPosition.GetPreviousPosition(); lateness /= 1000; return {"x": (curPos.x * (this.turnLength - lateness) + prevPos.x * lateness) / this.turnLength, "z": (curPos.z * (this.turnLength - lateness) + prevPos.z * lateness) / this.turnLength}; }; Attack.prototype.VectorDistance = function(p1, p2) { return Math.sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.z - p2.z)*(p1.z - p2.z)); }; Attack.prototype.VectorDot = function(p1, p2) { return (p1.x * p2.x + p1.z * p2.z); }; Attack.prototype.VectorCross = function(p1, p2) { return (p1.x * p2.z - p1.z * p2.x); }; Attack.prototype.VectorLength = function(p) { return Math.sqrt(p.x*p.x + p.z*p.z); }; // Tests whether it point is inside of ent's footprint Attack.prototype.testCollision = function(ent, point, lateness) { var targetPosition = this.InterpolatedLocation(ent, lateness); if (!targetPosition) return false; var cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); if (!cmpFootprint) return false; var targetShape = cmpFootprint.GetShape(); if (!targetShape || !targetPosition) return false; if (targetShape.type === 'circle') { return (this.VectorDistance(point, targetPosition) < targetShape.radius); } else { var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y; var dx = point.x - targetPosition.x; var dz = point.z - targetPosition.z; var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz; var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz; return (-targetShape.width/2 <= dxr && dxr < targetShape.width/2 && -targetShape.depth/2 <= dzr && dzr < targetShape.depth/2); } }; Attack.prototype.MissileHit = function(data, lateness) { var targetPosition = this.InterpolatedLocation(data.target, lateness); if (!targetPosition) return; if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target { var friendlyFire = this.template.Ranged.Splash.FriendlyFire; var splashRadius = this.template.Ranged.Splash.Range; var splashShape = this.template.Ranged.Splash.Shape; var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire); ents.push(data.target); // Add the original unit to the list of splash damage targets for (var i = 0; i < ents.length; i++) { var entityPosition = this.InterpolatedLocation(ents[i], lateness); var radius = this.VectorDistance(data.position, entityPosition); if (radius < splashRadius) { var multiplier = 1; if (splashShape == "Circular") // quadratic falloff { multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius)); } else if (splashShape == "Linear") { // position of entity relative to where the missile hit var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z}; var splashWidth = splashRadius / 5; var parallelDist = this.VectorDot(relPos, data.direction); var perpDist = Math.abs(this.VectorCross(relPos, data.direction)); // Check that the unit is within the distance splashWidth of the line starting at the missile's // landing point which extends in the direction of the missile for length splashRadius. if (parallelDist > -splashWidth && perpDist < splashWidth) { // Use a quadratic falloff in both directions multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius) * (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth); } else { multiplier = 0; } } var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier}; this.CauseDamage(newData); } } } if (this.testCollision(data.target, data.position, lateness)) { // Hit the primary target this.CauseDamage(data); // Remove the projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); cmpProjectileManager.RemoveProjectile(data.projectileId); } else { // If we didn't hit the main target look for nearby units var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2); for (var i = 0; i < ents.length; i++) { if (this.testCollision(ents[i], data.position, lateness)) { var newData = {"type": data.type, "target": ents[i]}; this.CauseDamage(newData); // Remove the projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); cmpProjectileManager.RemoveProjectile(data.projectileId); } } } }; Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var owner = cmpOwnership.GetOwner(); var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player); var numPlayers = cmpPlayerManager.GetNumPlayers(); var players = []; for (var i = 1; i < numPlayers; ++i) { // Only target enemies unless friendly fire is on if (cmpPlayer.IsEnemy(i) || friendlyFire) players.push(i); } var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver); } /** * Inflict damage on the target */ Attack.prototype.CauseDamage = function(data) { var strengths = this.GetAttackStrengths(data.type); var damageMultiplier = this.GetAttackBonus(data.type, data.target); if (data.damageMultiplier !== undefined) damageMultiplier *= data.damageMultiplier; var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); if (!cmpDamageReceiver) return; var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier); // if target killed pick up loot and credit experience if (targetState.killed == true) { this.TargetKilled(this.entity, data.target); } Engine.PostMessage(data.target, MT_Attacked, { "attacker": this.entity, "target": data.target, "type": data.type, "damage": -targetState.change }); PlaySound("attack_impact", this.entity); }; Attack.prototype.OnUpdate = function(msg) { this.turnLength = msg.turnLength; } Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 13625) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 13626) @@ -1,255 +1,296 @@ //Number of rounds of firing per 2 seconds const roundCount = 10; const timerInterval = 2000 / roundCount; function BuildingAI() {} BuildingAI.prototype.Schema = "" + "" + "" + "" + "" + ""; + /** * Initialize BuildingAI Component */ BuildingAI.prototype.Init = function() { if (this.GetDefaultArrowCount() > 0 || this.GetGarrisonArrowMultiplier() > 0) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.currentRound = 0; //Arrows left to fire this.arrowsLeft = 0; this.timer = cmpTimer.SetTimeout(this.entity, IID_BuildingAI, "FireArrows", timerInterval, {}); this.targetUnits = []; } }; BuildingAI.prototype.OnOwnershipChanged = function(msg) { // Remove current targets, to prevent them from being added twice this.targetUnits = []; if (msg.to != -1) this.SetupRangeQuery(msg.to); // Non-Gaia buildings should attack certain Gaia units. if (msg.to != 0 || this.gaiaUnitsQuery) this.SetupGaiaRangeQuery(msg.to); }; BuildingAI.prototype.OnDiplomacyChanged = function(msg) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) { // Remove maybe now allied/neutral units this.targetUnits = []; this.SetupRangeQuery(msg.player); } }; /** * Cleanup on destroy */ BuildingAI.prototype.OnDestroy = function() { if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } // Clean up range queries var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.enemyUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); if (this.gaiaUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery); }; /** * Setup the Range Query to detect units coming in & out of range */ BuildingAI.prototype.SetupRangeQuery = function(owner) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.enemyUnitsQuery) { cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); this.enemyUnitsQuery = undefined; } var players = []; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player); var numPlayers = cmpPlayerManager.GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) { // Exclude gaia, allies, and self // TODO: How to handle neutral players - Special query to attack military only? if (cmpPlayer.IsEnemy(i)) players.push(i); } var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (cmpAttack) { var range = cmpAttack.GetRange("Ranged"); - this.enemyUnitsQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal")); + this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery); } }; // Set up a range query for Gaia units within LOS range which can be attacked. // This should be called whenever our ownership changes. BuildingAI.prototype.SetupGaiaRangeQuery = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.gaiaUnitsQuery) { rangeMan.DestroyActiveQuery(this.gaiaUnitsQuery); this.gaiaUnitsQuery = undefined; } if (owner == -1) return; var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); if (!cmpPlayer.IsEnemy(0)) return; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (cmpAttack) { var range = cmpAttack.GetRange("Ranged"); // This query is only interested in Gaia entities that can attack. - this.gaiaUnitsQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal")); + this.gaiaUnitsQuery = rangeMan.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal")); rangeMan.EnableActiveQuery(this.gaiaUnitsQuery); } }; /** * Called when units enter or leave range */ BuildingAI.prototype.OnRangeUpdate = function(msg) { if (msg.tag == this.gaiaUnitsQuery) { const filter = function(e) { var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return (cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal())); }; if (msg.added.length) msg.added = msg.added.filter(filter); // Removed entities may not have cmpUnitAI. for (var i = 0; i < msg.removed.length; ++i) if (this.targetUnits.indexOf(msg.removed[i]) == -1) msg.removed.splice(i--, 1); } else if (msg.tag != this.enemyUnitsQuery) return; if (msg.added.length > 0) { for each (var entity in msg.added) { this.targetUnits.push(entity); } } if (msg.removed.length > 0) { for each (var entity in msg.removed) { this.targetUnits.splice(this.targetUnits.indexOf(entity), 1); } } }; BuildingAI.prototype.GetDefaultArrowCount = function() { var arrowCount = +this.template.DefaultArrowCount; return ApplyTechModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity); }; BuildingAI.prototype.GetGarrisonArrowMultiplier = function() { var arrowMult = +this.template.GarrisonArrowMultiplier; return ApplyTechModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity); }; /** * Returns the number of arrows which needs to be fired. * DefaultArrowCount + Garrisoned Archers(ie., any unit capable * of shooting arrows from inside buildings) */ BuildingAI.prototype.GetArrowCount = function() { var count = this.GetDefaultArrowCount(); var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) { count += Math.round(cmpGarrisonHolder.GetGarrisonedArcherCount() * this.GetGarrisonArrowMultiplier()); } return count; }; /** * Fires arrows. Called every N times every 2 seconds * where N is the number of Arrows */ BuildingAI.prototype.FireArrows = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (cmpAttack) { + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_BuildingAI, "FireArrows", timerInterval, {}); var arrowsToFire = 0; if (this.currentRound > (roundCount - 1)) { //Reached end of rounds. Reset count this.currentRound = 0; } if (this.currentRound == 0) { //First round. Calculate arrows to fire this.arrowsLeft = this.GetArrowCount(); } if (this.currentRound == (roundCount - 1)) { //Last round. Need to fire all left-over arrows arrowsToFire = this.arrowsLeft; } else { //Fire N arrows, 0 <= N <= Number of arrows left arrowsToFire = Math.floor(Math.random() * this.arrowsLeft); } + if (this.targetUnits.length > 0) { + var clonedTargets = this.targetUnits.slice(); for (var i = 0;i < arrowsToFire;i++) { - cmpAttack.PerformAttack("Ranged", this.targetUnits[Math.floor(Math.random() * this.targetUnits.length)]); - PlaySound("arrowfly", this.entity); + var target = clonedTargets[Math.floor(Math.random() * this.targetUnits.length)]; + if ( + target && + this.CheckTargetVisible(target) + ) + { + cmpAttack.PerformAttack("Ranged", target); + PlaySound("arrowfly", this.entity); + + } + else + { + clonedTargets.splice(clonedTargets.indexOf(target),1); + i--; // one extra arrow left to fire + if(clonedTargets.length < 1) + { + this.arrowsLeft += arrowsToFire; + // no targets found in this round, save arrows and go to next round + break; + } + } } this.arrowsLeft -= arrowsToFire; } this.currentRound++; } }; +/** + * Returns true if the target entity is visible through the FoW/SoD. + */ +BuildingAI.prototype.CheckTargetVisible = function(target) +{ + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership) + return false; + + var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + + if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden") + return false; + + // Either visible directly, or visible in fog + return true; +}; + Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 13625) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 13626) @@ -1,1775 +1,1820 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronised so we mustn't serialise // its non-deterministic data. Instead just return an empty object. return {}; }; GuiInterface.prototype.Deserialize = function(obj) { this.Init(); }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.rallyPoints = undefined; this.notifications = []; this.renamedEntities = []; }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function(player) { var ret = { "players": [] }; var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); // Work out what phase we are in var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); var phase = ""; if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; // store player ally/neutral/enemy data as arrays var allies = []; var mutualAllies = []; var neutrals = []; var enemies = []; for (var j = 0; j < n; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } var playerData = { "name": cmpPlayer.GetName(), "civ": cmpPlayer.GetCiv(), "colour": cmpPlayer.GetColour(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "heroes": cmpPlayer.GetHeroes(), "resourceCounts": cmpPlayer.GetResourceCounts(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits.GetLimits(), "entityCounts": cmpPlayerEntityLimits.GetCounts(), "techModifications": cmpTechnologyManager.GetTechModifications(), "researchQueued": cmpTechnologyManager.GetQueuedResearch(), "researchStarted": cmpTechnologyManager.GetStartedResearch(), "researchedTechs": cmpTechnologyManager.GetResearchedTechs(), "classCounts": cmpTechnologyManager.GetClassCounts(), "typeCountsByClass": cmpTechnologyManager.GetTypeCountsByClass() }; ret.players.push(playerData); } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) { ret.circularMap = cmpRangeManager.GetLosCircular(); } // Add timeElapsed var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); return ret; }; GuiInterface.prototype.GetExtendedSimulationState = function(player) { // Get basic simulation info var ret = this.GetSimulationState(); // Add statistics to each player var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics(); } return ret; }; GuiInterface.prototype.GetRenamedEntities = function(player) { return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function(player) { this.renamedEntities = []; }; GuiInterface.prototype.GetEntityState = function(player, ent) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // All units must have a template; if not then it's a nonexistent entity id var template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; var ret = { "id": ent, "template": template }; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) { ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName() }; } var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { ret.position = cmpPosition.GetPosition(); + ret.rotation = cmpPosition.GetRotation(); } var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = Math.ceil(cmpHealth.GetHitpoints()); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); ret.needsHeal = !cmpHealth.IsUnhealable(); } + var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpAttack = Engine.QueryInterface(ent, IID_Attack); + if (cmpAttack) { var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? show all? ret.attack = cmpAttack.GetAttackStrengths(type); var range = cmpAttack.GetRange(type); ret.attack.type = type; ret.attack.minRange = range.min; ret.attack.maxRange = range.max; + if (type == "Ranged") + { + ret.attack.elevationBonus = range.elevationBonus; + if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld()) + { + // For units, take the rage in front of it, no spread. So angle = 0 + ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(ret.position, ret.rotation, range.max, range.elevationBonus, 0); + } + else if(cmpPosition && cmpPosition.IsInWorld()) + { + // For buildings, take the average elevation around it. So angle = 2*pi + ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(ret.position, ret.rotation, range.max, range.elevationBonus, 2*Math.PI); + } + else + { + // not in world, set a default? + ret.attack.elevationAdaptedRange = ret.attack.maxRange; + } + + } + else + { + // not a ranged attack, set some defaults + ret.attack.elevationBonus = 0; + ret.attack.elevationAdaptedRange = ret.attack.maxRange; + } } var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); if (cmpArmour) { ret.armour = cmpArmour.GetArmourStrengths(); } var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) { ret.buildEntities = cmpBuilder.GetEntitiesList(); } var cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) { ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress(), }; } var cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) { ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "queue": cmpProductionQueue.GetQueue(), }; } var cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) { ret.trader = { "goods": cmpTrader.GetGoods(), "preferredGoods": cmpTrader.GetPreferredGoods() }; } var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation) { ret.foundation = { "progress": cmpFoundation.GetBuildPercentage(), "numBuilders": cmpFoundation.GetNumBuilders() }; } var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (cmpObstruction) { ret.obstruction = { "controlGroup": cmpObstruction.GetControlGroup(), "controlGroup2": cmpObstruction.GetControlGroup2(), }; } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) { ret.player = cmpOwnership.GetOwner(); } var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) { ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "gatherers": cmpResourceSupply.GetGatherers() }; } var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); } var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) { ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes() }; } var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { ret.rallyPoint = {'position': cmpRallyPoint.GetPositions()[0]}; // undefined or {x,z} object } var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) { ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "allowedClasses": cmpGarrisonHolder.GetAllowedClassesList(), "capacity": cmpGarrisonHolder.GetCapacity() }; } var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) { ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; } - - var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) { ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), }; // Add some information needed for ungarrisoning if (cmpUnitAI.isGarrisoned && ret.player) ret.template = "p" + ret.player + "&" + ret.template; } - + var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) { ret.gate = { "locked": cmpGate.IsLocked(), }; } if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket")) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); ret.barterMarket = { "prices": cmpBarter.GetPrices() }; } var cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) { ret.Healer = { "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses(), }; } - var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false); return ret; }; +GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) +{ + var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); + var rot = {x:0, y:0, z:0}; + var pos = {x:cmd.x,z:cmd.z}; + pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z); + var elevationBonus = cmd.elevationBonus || 0; + var range = cmd.range; + + return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI); +}; + + + GuiInterface.prototype.GetTemplateData = function(player, extendedName) { var name = extendedName; // Special case for garrisoned units which have a extended template if (extendedName.indexOf("&") != -1) name = extendedName.slice(extendedName.indexOf("&")+1); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(name); if (!template) return null; var ret = {}; var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); var techMods = cmpTechnologyManager.GetTechModifications(); if (template.Armour) { ret.armour = { "hack": GetTechModifiedProperty(techMods, template, "Armour/Hack", +template.Armour.Hack), "pierce": GetTechModifiedProperty(techMods, template, "Armour/Pierce", +template.Armour.Pierce), "crush": GetTechModifiedProperty(techMods, template, "Armour/Crush", +template.Armour.Crush), }; } if (template.Attack) { ret.attack = {}; for (var type in template.Attack) { ret.attack[type] = { "hack": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Hack", +(template.Attack[type].Hack || 0)), "pierce": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Pierce", +(template.Attack[type].Pierce || 0)), "crush": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0)), "minRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0)), "maxRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange), + "elevationBonus": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0)), }; } } if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromCategory": template.BuildRestrictions.Distance.FromCategory, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance; if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance; } } if (template.TrainingRestrictions) { ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; } if (template.Cost) { ret.cost = {}; if (template.Cost.Resources.food) ret.cost.food = GetTechModifiedProperty(techMods, template, "Cost/Resources/food", +template.Cost.Resources.food); if (template.Cost.Resources.wood) ret.cost.wood = GetTechModifiedProperty(techMods, template, "Cost/Resources/wood", +template.Cost.Resources.wood); if (template.Cost.Resources.stone) ret.cost.stone = GetTechModifiedProperty(techMods, template, "Cost/Resources/stone", +template.Cost.Resources.stone); if (template.Cost.Resources.metal) ret.cost.metal = GetTechModifiedProperty(techMods, template, "Cost/Resources/metal", +template.Cost.Resources.metal); if (template.Cost.Population) ret.cost.population = GetTechModifiedProperty(techMods, template, "Cost/Population", +template.Cost.Population); if (template.Cost.PopulationBonus) ret.cost.populationBonus = GetTechModifiedProperty(techMods, template, "Cost/PopulationBonus", +template.Cost.PopulationBonus); if (template.Cost.BuildTime) ret.cost.time = GetTechModifiedProperty(techMods, template, "Cost/BuildTime", +template.Cost.BuildTime); } if (template.Footprint) { ret.footprint = {"height": template.Footprint.Height}; if (template.Footprint.Square) ret.footprint.square = {"width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"]}; else if (template.Footprint.Circle) ret.footprint.circle = {"radius": +template.Footprint.Circle["@radius"]}; else warn("[GetTemplateData] Unrecognized Footprint type"); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else { ret.obstruction.shape.type = "cluster"; } } if (template.Pack) { ret.pack = { "state": template.Pack.State, "time": GetTechModifiedProperty(techMods, template, "Pack/Time", +template.Pack.Time), }; } if (template.Health) { ret.health = Math.round(GetTechModifiedProperty(techMods, template, "Health/Max", +template.Health.Max)); } if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.identityClassesString = GetTemplateIdentityClassesString(template); } if (template.UnitMotion) { ret.speed = { "walk": +template.UnitMotion.WalkSpeed, }; if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed; } if (template.Trader) ret.trader = template.Trader; if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort, }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap, }; } if (template.WallPiece) { ret.wallPiece = {"length": +template.WallPiece.Length}; } return ret; }; GuiInterface.prototype.GetTechnologyData = function(player, name) { var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager); var template = cmpTechTempMan.GetTemplate(name); if (!template) { warn("Tried to get data for invalid technology: " + name); return null; } var ret = {}; // Get specific name for this civ or else the generic specific name var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var specific = undefined; if (template.specificName) { if (template.specificName[cmpPlayer.GetCiv()]) specific = template.specificName[cmpPlayer.GetCiv()]; else specific = template.specificName['generic']; } ret.name = { "specific": specific, "generic": template.genericName, }; ret.icon = "technologies/" + template.icon; ret.cost = { "food": template.cost ? (+template.cost.food) : 0, "wood": template.cost ? (+template.cost.wood) : 0, "metal": template.cost ? (+template.cost.metal) : 0, "stone": template.cost ? (+template.cost.stone) : 0, "time": template.researchTime ? (+template.researchTime) : 0, } ret.tooltip = template.tooltip; if (template.requirementsTooltip) ret.requirementsTooltip = template.requirementsTooltip; else ret.requirementsTooltip = ""; ret.description = template.description; return ret; }; GuiInterface.prototype.IsTechnologyResearched = function(player, tech) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(tech); }; // Checks whether the requirements for this technology have been met GuiInterface.prototype.CheckTechnologyRequirements = function(player, tech) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(tech); }; // Returns technologies that are being actively researched, along with // which entity is researching them and how far along the research is. GuiInterface.prototype.GetStartedResearch = function(player) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; var ret = {}; for (var tech in cmpTechnologyManager.GetTechsStarted()) { ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) }; var cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue); if (cmpProductionQueue) ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress; else ret[tech].progress = 0; } return ret; } // Returns the battle state of the player. GuiInterface.prototype.GetBattleState = function(player) { var cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); return cmpBattleDetection.GetState(); }; // Used to show a red square over GUI elements you can't yet afford. GuiInterface.prototype.GetNeededResources = function(player, amounts) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); return cmpPlayer.GetNeededResources(amounts); }; GuiInterface.prototype.PushNotification = function(notification) { this.notifications.push(notification); }; GuiInterface.prototype.GetNextNotification = function() { if (this.notifications.length) return this.notifications.pop(); else return ""; }; GuiInterface.prototype.GetAvailableFormations = function(player, data) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); return cmpPlayer.GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationName); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationName); }; GuiInterface.prototype.IsFormationSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { // GetLastFormationName is named in a strange way as it (also) is // the value of the current formation (see Formation.js LoadFormation) if (cmpUnitAI.GetLastFormationName() == data.formationName) return true; } } return false; }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmpUnitAI.GetStanceName() == data.stance) return true; } } return false; }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd, selected) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerColours = {}; // cache of owner -> colour map for each (var ent in cmd.entities) { var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's colour: var owner = -1; var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); var colour = playerColours[owner]; if (!colour) { colour = {"r":1, "g":1, "b":1}; var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player); if (cmpPlayer) colour = cmpPlayer.GetColour(); playerColours[owner] = colour; } cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha}, cmd.selected); } }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { for each (var ent in cmd.entities) { var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.SetEnabled(cmd.enabled); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); // If there are some rally points already displayed, first hide them for each (var ent in this.entsRallyPointsDisplayed) { var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities for each (var ent in cmd.entities) { var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location) var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position var pos; if (cmd.x && cmd.z) pos = cmd; else pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set if (pos) { // Only update the position if we changed it (cmd.queued is set) if (cmd.queued == true) cmpRallyPointRenderer.AddPosition({'x': pos.x, 'y': pos.z}); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z else if (cmd.queued == false) cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z cmpRallyPointRenderer.SetDisplayed(true); // remember which entities have their rally points displayed so we can hide them again this.entsRallyPointsDisplayed.push(ent); } } }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else empty string * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { var result = { "success": false, "message": "", } // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) { // Destroy the old preview if there was one if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); // Load the new template if (cmd.template == "") { this.placementEntity = undefined; } else { this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } } if (this.placementEntity) { var ent = this.placementEntity[1]; // Move the preview into the right location var pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); // Set it to a red shade if this is an invalid location var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColour(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * 'populationBonus': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { var wallSet = cmd.wallSet; var start = { "pos": cmd.start, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; var end = { "pos": cmd.end, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; // -------------------------------------------------------------------------------- // do some entity cache management and check for snapping if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // we're clearing the preview, clear the entity cache and bail var numCleared = 0; for (var tpl in this.placementWallEntities) { for each (var ent in this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // keep template data around } return false; } else { // Move all existing cached entities outside of the world and reset their use count for (var tpl in this.placementWallEntities) { for each (var ent in this.placementWallEntities[tpl].entities) { var pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before for each (var tpl in wallSet.templates) { if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, tpl), }; // ensure that the loaded template data contains a wallPiece component if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } } // prevent division by zero errors further on if the start and end positions are the same if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { var snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error var startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { var endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // clear the single-building preview entity (we'll be rolling our own) this.SetBuildingPlacementPreview(player, {"template": ""}); // -------------------------------------------------------------------------------- // calculate wall placement and position preview entities var result = { "pieces": [], "cost": {"food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0}, }; var previewEntities = []; if (end.pos) previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { var startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length > 0 && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group var startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { var cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) { previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)], "excludeFromResult": true, // preview only, must not appear in the result }); } } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle) }); } if (end.pos) { // Analogous to the starting side case above if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { var endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []); previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup()); } // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group var endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { var cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) { previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)], "excludeFromResult": true }); } } } else { previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle) }); } } var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. var allPiecesValid = true; var numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity for (var i = 0; i < previewEntities.length; ++i) { var entInfo = previewEntities[i]; var ent = null; var tpl = entInfo.template; var tplData = this.placementWallEntities[tpl].templateData; var entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { // allocate new entity ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else { // reuse an existing one ent = entPool.entities[entPool.numUsed]; } if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // move piece to right location // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces if (tpl === wallSet.templates.tower) { var terrainGroundPrev = null; var terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { var targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. var primaryControlGroup = ent; var secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); // check whether this wall piece can be validly positioned here var validPlacement = false; var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region // tell GetLosVisibility to force RetainInFog because preview entities set this to false, // which would show them as hidden instead of fogged // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta var visible = (cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden"); if (visible) { var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success); // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) numRequiredPieces++; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. result.cost.food += tplData.cost.food; result.cost.wood += tplData.cost.wood; result.cost.stone += tplData.cost.stone; result.cost.metal += tplData.cost.metal; result.cost.population += tplData.cost.population; result.cost.populationBonus += tplData.cost.populationBonus; result.cost.time += tplData.cost.time; } var canAfford = true; var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) var canAfford = false; var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColour(1, 1, 1, 1); } entPool.numUsed++; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateMgr.GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest // (TODO: break unlikely ties by choosing the lowest entity ID) var minDist2 = -1; var minDistEntitySnapData = null; var radius2 = data.snapRadius * data.snapRadius; for each (var ent in data.snapEntities) { var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; var pos = cmpPosition.GetPosition(); var dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = {"x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent}; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (template.BuildRestrictions.Category == "Dock") { // warning: copied almost identically in helpers/command.js , "GetDockAngle". var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) { return false; } // Get footprint size var halfSize = 0; if (template.Footprint.Square) { halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; } else if (template.Footprint.Circle) { halfSize = template.Footprint.Circle["@radius"]; } /* Find direction of most open water, algorithm: * 1. Pick points in a circle around dock * 2. If point is in water, add to array * 3. Scan array looking for consecutive points * 4. Find longest sequence of consecutive points * 5. If sequence equals all points, no direction can be determined, * expand search outward and try (1) again * 6. Calculate angle using average of sequence */ const numPoints = 16; for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { var angle = (i/numPoints)*2*Math.PI; var d = halfSize*(dist+1); var nx = data.x - d*Math.sin(angle); var nz = data.z + d*Math.cos(angle); if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) { waterPoints.push(i); } } var consec = []; var length = waterPoints.length; for (var i = 0; i < length; ++i) { var count = 0; for (var j = 0; j < (length-1); ++j) { if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length]) { ++count; } else { break; } } consec[i] = count; } var start = 0; var count = 0; for (var c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) { return {"x": data.x, "z": data.z, "angle": -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI)}; } } } return false; }; GuiInterface.prototype.PlaySound = function(player, data) { // Ignore if no entity was passed if (!data.entity) return; PlaySound(data.name, data.entity); }; function isIdleUnit(ent, idleClass) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: Do something with garrisoned idle units return (cmpUnitAI && cmpIdentity && cmpUnitAI.IsIdle() && !cmpUnitAI.IsGarrisoned() && idleClass && cmpIdentity.HasClass(idleClass)); } GuiInterface.prototype.FindIdleUnits = function(player, data) { var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerEntities = rangeMan.GetEntitiesByPlayer(player).filter( function(e) { var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); if (cmpUnitAI) return true; return false; }); var idleUnits = []; var noFilter = (data.prevUnit == undefined && data.excludeUnits == undefined); for (var j = 0; j < playerEntities.length; ++j) { var ent = playerEntities[j]; if (!isIdleUnit(ent, data.idleClass)) continue; if (noFilter || ((data.prevUnit == undefined || ent > data.prevUnit) && (data.excludeUnits == undefined || data.excludeUnits.indexOf(ent) == -1))) { idleUnits.push(ent); playerEntities.splice(j--, 1); } if (data.limit && idleUnits.length >= data.limit) break; } return idleUnits; } GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template); } GuiInterface.prototype.GetTradingDetails = function(player, data) { var cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; var firstMarket = cmpEntityTrader.GetFirstMarket(); var secondMarket = cmpEntityTrader.GetSecondMarket(); var result = null; if (data.target === firstMarket) { result = { "type": "is first", "goods": cmpEntityTrader.GetPreferredGoods(), "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGain(); } else if (data.target === secondMarket) { result = { "type": "is second", "gain": cmpEntityTrader.GetGain(), "goods": cmpEntityTrader.GetPreferredGoods() }; } else if (!firstMarket) { result = {"type": "set first"}; } else if (!secondMarket) { result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), "goods": cmpEntityTrader.GetPreferredGoods() }; } else { // Else both markets are not null and target is different from them result = {"type": "set first"}; } return result; }; GuiInterface.prototype.CanAttack = function(player, data) { var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); if (!cmpAttack) return false; return cmpAttack.CanAttack(data.target); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { var cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue); if (!cmpProductionQueue) return 0; return cmpProductionQueue.GetBatchTime(data.batchSize); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); cmpPathfinder.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); cmpObstructionManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for each (var ent in data.entities) { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); } // List the GuiInterface functions that can be safely called by GUI scripts. // (GUI scripts are non-deterministic and untrusted, so these functions must be // appropriately careful. They are called with a first argument "player", which is // trusted and indicates the player associated with the current client; no data should // be returned unless this player is meant to be able to see it.) var exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, + "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "GetTechnologyData": 1, "IsTechnologyResearched": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetNeededResources": 1, "GetNextNotification": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "IsStanceSelected": 1, "SetSelectionHighlight": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "DisplayRallyPoint": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "SetPathfinderDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); else throw new Error("Invalid GuiInterface Call name \""+name+"\""); }; Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 13625) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 13626) @@ -1,4471 +1,4571 @@ function UnitAI() {} UnitAI.prototype.Schema = "Controls the unit's movement, attacks, etc, in response to commands from the player." + "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "standground" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "skittish" + "domestic" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + ""; // Unit stances. // There some targeting options: // targetVisibleEnemies: anything in vision range is a viable target // targetAttackersAlways: anything that hurts us is a viable target, // possibly overriding user orders! // targetAttackersPassive: anything that hurts us is a viable target, // if we're on a passive/unforced order (e.g. gathering/building) // There are some response options, triggered when targets are detected: // respondFlee: run away // respondChase: start chasing after the enemy // respondChaseBeyondVision: start chasing, and don't stop even if it's out // of this unit's vision range (though still visible to the player) // respondStandGround: attack enemy but don't move at all // respondHoldGround: attack enemy but don't move far from current position // TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts, // do worry around armies slaughtering the guy standing next to you), etc. var g_Stances = { "violent": { targetVisibleEnemies: true, targetAttackersAlways: true, targetAttackersPassive: true, respondFlee: false, respondChase: true, respondChaseBeyondVision: true, respondStandGround: false, respondHoldGround: false, }, "aggressive": { targetVisibleEnemies: true, targetAttackersAlways: false, targetAttackersPassive: true, respondFlee: false, respondChase: true, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: false, }, "defensive": { targetVisibleEnemies: true, targetAttackersAlways: false, targetAttackersPassive: true, respondFlee: false, respondChase: false, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: true, }, "passive": { targetVisibleEnemies: false, targetAttackersAlways: false, targetAttackersPassive: true, respondFlee: true, respondChase: false, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: false, }, "standground": { targetVisibleEnemies: true, targetAttackersAlways: false, targetAttackersPassive: true, respondFlee: false, respondChase: false, respondChaseBeyondVision: false, respondStandGround: true, respondHoldGround: false, }, }; // See ../helpers/FSM.js for some documentation of this FSM specification syntax var UnitFsmSpec = { // Default event handlers: "MoveCompleted": function() { // ignore spurious movement messages // (these can happen when stopping moving at the same time // as switching states) }, "MoveStarted": function() { // ignore spurious movement messages }, "ConstructionFinished": function(msg) { // ignore uninteresting construction messages }, "LosRangeUpdate": function(msg) { // ignore newly-seen units by default }, "LosGaiaRangeUpdate": function(msg) { // ignore newly-seen Gaia units by default }, "LosHealRangeUpdate": function(msg) { // ignore newly-seen injured units by default }, "Attacked": function(msg) { // ignore attacker }, "HealthChanged": function(msg) { // ignore }, "PackFinished": function(msg) { // ignore }, // Formation handlers: "FormationLeave": function(msg) { // ignore when we're not in FORMATIONMEMBER }, // Called when being told to walk as part of a formation "Order.FormationWalk": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z); this.SetNextState("FORMATIONMEMBER.WALKING"); }, // Special orders: // (these will be overridden by various states) "Order.LeaveFoundation": function(msg) { // If foundation is not ally of entity, or if entity is unpacked siege, // ignore the order if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) || this.IsPacking() || this.CanPack()) { this.FinishOrder(); return; } // Move a tile outside the building var range = 4; var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range); if (ok) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, // Individual orders: // (these will switch the unit out of formation mode) "Order.Stop": function(msg) { // We have no control over non-domestic animals. if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } // Stop moving immediately. this.StopMoving(); this.FinishOrder(); // No orders left, we're an individual now if (this.IsAnimal()) this.SetNextState("ANIMAL.IDLE"); else this.SetNextState("INDIVIDUAL.IDLE"); }, "Order.Walk": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } this.SetHeldPosition(this.order.data.x, this.order.data.z); this.MoveToPoint(this.order.data.x, this.order.data.z); if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); else this.SetNextState("INDIVIDUAL.WALKING"); }, "Order.WalkAndFight": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } this.SetHeldPosition(this.order.data.x, this.order.data.z); this.MoveToPoint(this.order.data.x, this.order.data.z); if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); // WalkAndFight not applicable for animals else this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING"); }, "Order.WalkToTarget": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } var ok = this.MoveToTarget(this.order.data.target); if (ok) { // We've started walking to the given point if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); else this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.StopMoving(); this.FinishOrder(); } }, "Order.Flee": function(msg) { // We use the distance between the enities to account for ranged attacks var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance); var ok = this.MoveToTargetRangeExplicit(this.order.data.target, distance, -1); if (ok) { // We've started fleeing from the given target if (this.IsAnimal()) this.SetNextState("ANIMAL.FLEEING"); else this.SetNextState("INDIVIDUAL.FLEEING"); } else { // We are already at the target, or can't move at all this.StopMoving(); this.FinishOrder(); } }, "Order.Attack": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Work out how to attack the given target var type = this.GetBestAttackAgainst(this.order.data.target); if (!type) { // Oops, we can't attack at all this.FinishOrder(); return; } this.order.data.attackType = type; // If we are already at the target, try attacking it from here - if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType)) + if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType)) { this.StopMoving(); // For packable units within attack range: // 1. If unpacked, we can attack the target. // 2. If packed, we first need to unpack, then follow case 1. if (this.CanUnpack()) { // Ignore unforced attacks // this would prevent attacks from AttackVisibleEntity or AttackEntityInZone ? // so we accept attacks against targets for which we have a bonus // TODO: use special stances instead? if (!this.order.data.force && this.GetAttackBonus(type, this.order.data.target) < 1.5) { this.FinishOrder(); return; } // Case 2: unpack this.PushOrderFront("Unpack", { "force": true }); return; } if (this.IsAnimal()) this.SetNextState("ANIMAL.COMBAT.ATTACKING"); else this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); return; } // For packable units out of attack range: // 1. If packed, we need to move to attack range and then unpack. // 2. If unpacked, we first need to pack, then follow case 1. var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack) { // Ignore unforced attacks // TODO: use special stances instead? if (!this.order.data.force) { this.FinishOrder(); return; } if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } } // If we can't reach the target, but are standing ground, then abandon this attack order. // Unless we're hunting, that's a special case where we should continue attacking our target. if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting) { this.FinishOrder(); return; } // Try to move within attack range - if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType)) + if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType,0.5)) { // We've started walking to the given point if (this.IsAnimal()) this.SetNextState("ANIMAL.COMBAT.APPROACHING"); else this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); return; } // We can't reach the target, and can't move towards it, // so abandon this attack order this.FinishOrder(); }, "Order.Heal": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Healers can't heal themselves. if (this.order.data.target == this.entity) { this.FinishOrder(); return; } // Check if the target is in range if (this.CheckTargetRange(this.order.data.target, IID_Heal)) { this.StopMoving(); this.SetNextState("INDIVIDUAL.HEAL.HEALING"); return; } // If we can't reach the target, but are standing ground, // then abandon this heal order if (this.GetStance().respondStandGround && !this.order.data.force) { this.FinishOrder(); return; } // Try to move within heal range if (this.MoveToTargetRange(this.order.data.target, IID_Heal)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.HEAL.APPROACHING"); return; } // We can't reach the target, and can't move towards it, // so abandon this heal order this.FinishOrder(); }, "Order.Gather": function(msg) { // If the target is still alive, we need to kill it first if (this.MustKillGatherTarget(this.order.data.target)) { // Make sure we can attack the target, else we'll get very stuck if (!this.GetBestAttackAgainst(this.order.data.target)) { // Oops, we can't attack at all - give up // TODO: should do something so the player knows why this failed this.FinishOrder(); return; } // The target was visible when this order was issued, // but could now be invisible again. if (!this.CheckTargetVisible(this.order.data.target)) { if (this.order.data.secondTry === undefined) { this.order.data.secondTry = true; this.PushOrderFront("Walk", this.order.data.lastPos); } else { // We couldn't move there, or the target moved away this.FinishOrder(); } return; } this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true }); return; } // Try to move within range if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.GATHER.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try gathering it from here. // TODO: need better handling of the can't-reach-target case this.StopMoving(); this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING"); } }, "Order.GatherNearPosition": function(msg) { // Move the unit to the position to gather from. this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("INDIVIDUAL.GATHER.WALKING"); }, "Order.ReturnResource": function(msg) { // Try to move to the dropsite if (this.MoveToTarget(this.order.data.target)) { // We've started walking to the target this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING"); } else { // Oops, we can't reach the dropsite. // Maybe we should try to pick another dropsite, to find an // accessible one? // For now, just give up. this.StopMoving(); this.FinishOrder(); return; } }, "Order.Trade": function(msg) { if (this.MoveToMarket(this.order.data.firstMarket)) { // We've started walking to the first market this.SetNextState("INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET"); } }, "Order.Repair": function(msg) { // Try to move within range if (this.MoveToTargetRange(this.order.data.target, IID_Builder)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try repairing it from here. // TODO: need better handling of the can't-reach-target case this.StopMoving(); this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING"); } }, "Order.Garrison": function(msg) { // For packable units: // 1. If packed, we can move to the garrison target. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } if (this.MoveToTarget(this.order.data.target)) { this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); } else { // We do a range check before actually garrisoning this.StopMoving(); this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED"); } }, "Order.Cheering": function(msg) { this.SetNextState("INDIVIDUAL.CHEERING"); }, "Order.Pack": function(msg) { if (this.CanPack()) { this.StopMoving(); this.SetNextState("INDIVIDUAL.PACKING"); } }, "Order.Unpack": function(msg) { if (this.CanUnpack()) { this.StopMoving(); this.SetNextState("INDIVIDUAL.UNPACKING"); } }, "Order.CancelPack": function(msg) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) cmpPack.CancelPack(); this.FinishOrder(); }, "Order.CancelUnpack": function(msg) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) cmpPack.CancelPack(); this.FinishOrder(); }, // States for the special entity representing a group of units moving in formation: "FORMATIONCONTROLLER": { "Order.Walk": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKING"); }, "Order.WalkAndFight": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKINGANDFIGHTING"); }, "Order.MoveIntoFormation": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("FORMING"); }, // Only used by other orders to walk there in formation "Order.WalkToTargetRange": function(msg) { if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max)) this.SetNextState("WALKING"); else this.FinishOrder(); }, "Order.WalkToPointRange": function(msg) { if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max)) this.SetNextState("WALKING"); else this.FinishOrder(); }, "Order.Stop": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("Stop", [false]); cmpFormation.Disband(); }, "Order.Attack": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); var maxRange = cmpFormation.GetMaxAttackRangeFunction(msg.data.target); // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, maxRange)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target was destroyed or isn't visible any more. this.FinishOrder(); else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": maxRange }); return; } // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Attack", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Garrison": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target was destroyed this.FinishOrder(); else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Gather": function(msg) { if (this.MustKillGatherTarget(msg.data.target)) { // The target was visible when this order was given, // but could now be invisible. if (!this.CheckTargetVisible(msg.data.target)) { if (msg.data.secondTry === undefined) { msg.data.secondTry = true; this.PushOrderFront("Walk", msg.data.lastPos); } else { // We couldn't move there, or the target moved away this.FinishOrder(); } return; } this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true }); return; } // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target isn't gatherable or not visible any more. this.FinishOrder(); // TODO: Should we issue a gather-near-position order // if the target isn't gatherable/doesn't exist anymore? else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.GatherNearPosition": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20)) { // Out of range; move there in formation this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 }); return; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Heal": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target was destroyed this.FinishOrder(); else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Repair": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The building was finished or destroyed this.FinishOrder(); else // Out of range move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.ReturnResource": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target was destroyed this.FinishOrder(); else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("ReturnResource", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Pack": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Pack", [false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Unpack": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // We don't want to rearrange the formation if the individual units are carrying // out a task and one of the members dies/leaves the formation. cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Unpack", [false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "IDLE": { }, "WALKING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (this.FinishOrder()) { cmpFormation.CallMemberFunction("ResetFinishOrder", []); return; } // No more orders left. cmpFormation.Disband(); }, }, "WALKINGANDFIGHTING": { "enter": function(msg) { this.StartTimer(0, 1000); }, "Timer": function(msg) { // check if there are no enemies to attack var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); for each (var ent in cmpFormation.members) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI.FindNewTargets()) { if (cmpUnitAI.orderQueue[0] && cmpUnitAI.orderQueue[0].type == "Attack") { var data = cmpUnitAI.orderQueue[0].data; cmpUnitAI.FinishOrder(); this.PushOrderFront("Attack", { "target": data.target, "force": false, "forceResponse": data.forceResponse }); break; } } } }, "leave": function(msg) { this.StopTimer(); }, "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (this.FinishOrder()) { cmpFormation.CallMemberFunction("ResetFinishOrder", []); return; } // No more orders left. cmpFormation.Disband(); }, }, "FORMING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, false); }, "MoveCompleted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (this.FinishOrder()) { cmpFormation.CallMemberFunction("ResetFinishOrder", []); return; } // If this was the last order, attempt to disband the formation. cmpFormation.FindInPosition(); } }, "MEMBER": { // Wait for individual members to finish "enter": function(msg) { this.StartTimer(1000, 1000); }, "Timer": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // Have all members finished the task? if (!cmpFormation.TestAllMemberFunction("HasFinishedOrder", [])) return; cmpFormation.CallMemberFunction("ResetFinishOrder", []); // Execute the next order if (this.FinishOrder()) { // if WalkAndFight order, look for new target before moving again if (this.orderQueue.length > 0 && this.orderQueue[0].type == "WalkAndFight") { for each (var ent in cmpFormation.members) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI.FindNewTargets()) { if (cmpUnitAI.orderQueue[0] && cmpUnitAI.orderQueue[0].type == "Attack") { var data = cmpUnitAI.orderQueue[0].data; cmpUnitAI.FinishOrder(); this.PushOrderFront("Attack", { "target": data.target, "force": false, "forceResponse": data.forceResponse }); break; } } } } return; } // No more order left. cmpFormation.Disband(); }, "leave": function(msg) { this.StopTimer(); }, }, }, // States for entities moving as part of a formation: "FORMATIONMEMBER": { "FormationLeave": function(msg) { // We're not in a formation anymore, so no need to track this. this.finishedOrder = false; // Stop moving as soon as the formation disbands this.StopMoving(); // If the controller handled an order but some members rejected it, // they will have no orders and be in the FORMATIONMEMBER.IDLE state. if (this.orderQueue.length) { // We're leaving the formation, so stop our FormationWalk order if (this.FinishOrder()) return; } // No orders left, we're an individual now if (this.IsAnimal()) this.SetNextState("ANIMAL.IDLE"); else this.SetNextState("INDIVIDUAL.IDLE"); }, // Override the LeaveFoundation order since we're not doing // anything more important (and we might be stuck in the WALKING // state forever and need to get out of foundations in that case) "Order.LeaveFoundation": function(msg) { // If foundation is not ally of entity, or if entity is unpacked siege, // ignore the order if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) || this.IsPacking() || this.CanPack()) { this.FinishOrder(); return; } // Move a tile outside the building var range = 4; var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range); if (ok) { // We've started walking to the given point this.SetNextState("WALKINGTOPOINT"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, "IDLE": { "enter": function() { this.SelectAnimation("idle"); }, }, "WALKING": { "enter": function () { this.SelectAnimation("move"); }, // Occurs when the unit has reached its destination and the controller // is done moving. The controller is notified, and will disband the // formation if all units are in formation and no orders remain. "MoveCompleted": function(msg) { // We can only finish this order if the move was really completed. if (!msg.data.error && this.FinishOrder()) return; var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.SetInPosition(this.entity); }, }, // Special case used by Order.LeaveFoundation "WALKINGTOPOINT": { "enter": function() { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.UnsetInPosition(this.entity); this.SelectAnimation("move"); }, "MoveCompleted": function() { this.FinishOrder(); }, }, }, // States for entities not part of a formation: "INDIVIDUAL": { "enter": function() { // Sanity-checking if (this.IsAnimal()) error("Animal got moved into INDIVIDUAL.* state"); }, "Attacked": function(msg) { // Respond to attack if we always target attackers, or if we target attackers // during passive orders (e.g. gathering/repairing are never forced) if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (!this.order || !this.order.data || !this.order.data.force))) { this.RespondToTargetedEntities([msg.data.attacker]); } }, "IDLE": { "enter": function() { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation this.SelectAnimation("idle"); // The GUI and AI want to know when a unit is idle, but we don't // want to send frequent spurious messages if the unit's only // idle for an instant and will quickly go off and do something else. // So we'll set a timer here and only report the idle event if we // remain idle this.StartTimer(1000); // If a unit can heal and attack we first want to heal wounded units, // so check if we are a healer and find whether there's anybody nearby to heal. // (If anyone approaches later it'll be handled via LosHealRangeUpdate.) // If anyone in sight gets hurt that will be handled via LosHealRangeUpdate. if (this.IsHealer() && this.FindNewHealTargets()) return true; // (abort the FSM transition since we may have already switched state) // If we entered the idle state we must have nothing better to do, // so immediately check whether there's anybody nearby to attack. // (If anyone approaches later, it'll be handled via LosRangeUpdate.) if (this.FindNewTargets()) return true; // (abort the FSM transition since we may have already switched state) // Nobody to attack - stay in idle return false; }, "leave": function() { var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) rangeMan.DisableActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) rangeMan.DisableActiveQuery(this.losHealRangeQuery); this.StopTimer(); if (this.isIdle) { this.isIdle = false; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, "LosRangeUpdate": function(msg) { if (this.GetStance().targetVisibleEnemies) { // Start attacking one of the newly-seen enemy (if any) this.AttackEntitiesByPreference(msg.data.added); } }, "LosGaiaRangeUpdate": function(msg) { if (this.GetStance().targetVisibleEnemies) { // Start attacking one of the newly-seen enemy (if any) this.AttackGaiaEntitiesByPreference(msg.data.added); } }, "LosHealRangeUpdate": function(msg) { this.RespondToHealableEntities(msg.data.added); }, "Timer": function(msg) { if (!this.isIdle) { this.isIdle = true; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, }, "WALKING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.FinishOrder(); }, }, "WALKINGANDFIGHTING": { "enter": function () { this.StartTimer(0, 1000); this.SelectAnimation("move"); }, "Timer": function(msg) { this.FindNewTargets(); }, "leave": function(msg) { this.StopTimer(); }, "MoveCompleted": function() { this.FinishOrder(); }, }, "FLEEING": { "enter": function() { this.PlaySound("panic"); // Run quickly var speed = this.GetRunSpeed(); this.SelectAnimation("move"); this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed this.SetMoveSpeed(this.GetWalkSpeed()); }, "MoveCompleted": function() { // When we've run far enough, stop fleeing this.FinishOrder(); }, // TODO: what if we run into more enemies while fleeing? }, "COMBAT": { "Order.LeaveFoundation": function(msg) { // Ignore the order as we're busy. return { "discardOrder": true }; }, "Attacked": function(msg) { // If we're already in combat mode, ignore anyone else // who's attacking us }, "APPROACHING": { "enter": function () { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { // Show carried resources when walking. this.SetGathererAnimationOverride(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { - // If the unit needs to unpack, do so - if (this.CanUnpack()) - this.SetNextState("UNPACKING"); - else - this.SetNextState("ATTACKING"); + + if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack , this.order.data.attackType)) + { + // If the unit needs to unpack, do so + if (this.CanUnpack()) + this.SetNextState("UNPACKING"); + else + this.SetNextState("ATTACKING"); + } + else + { + if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType,0)) + { + this.SetNextState("APPROACHING"); + } + else + { + // Give up + this.FinishOrder(); + } + } }, "Attacked": function(msg) { // If we're attacked by a close enemy, we should try to defend ourself // but only if we're not forced to target something else if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force))) { this.RespondToTargetedEntities([msg.data.attacker]); } }, }, "UNPACKING": { "enter": function() { // If we're not in range yet (maybe we stopped moving), move to target again - if (!this.CheckTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType)) + if (!this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType)) { - if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType)) + if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType,0.5)) this.SetNextState("APPROACHING"); else { // Give up this.FinishOrder(); } return true; } // In range, unpack var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Unpack(); return false; }, "PackFinished": function(msg) { this.SetNextState("ATTACKING"); }, "leave": function() { }, "Attacked": function(msg) { // Ignore further attacks while unpacking }, }, "ATTACKING": { "enter": function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType); // If the repeat time since the last attack hasn't elapsed, // delay this attack to avoid attacking too fast. var prepare = this.attackTimers.prepare; if (this.lastAttacked) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } // add prefix + no capital first letter for attackType var attackName = "attack_" + this.order.data.attackType.toLowerCase(); this.SelectAnimation(attackName, false, 1.0, "attack"); this.SetAnimationSync(prepare, this.attackTimers.repeat); this.StartTimer(prepare, this.attackTimers.repeat); // TODO: we should probably only bother syncing projectile attacks, not melee // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false; this.FaceTowardsTarget(this.order.data.target); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { var target = this.order.data.target; // Check the target is still alive and attackable if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null)) { // Check we can still reach the target - if (this.CheckTargetRange(target, IID_Attack, this.order.data.attackType)) + if (this.CheckTargetAttackRange(target, IID_Attack, this.order.data.attackType)) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastAttacked = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); cmpAttack.PerformAttack(this.order.data.attackType, target); if (this.resyncAnimation) { this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat); this.resyncAnimation = false; } return; } // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetRange(target, IID_Attack, this.order.data.attackType)) { this.SetNextState("COMBAT.CHASING"); return; } } } // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up // Except if in WalkAndFight mode where we look for more ennemies around before moving again if (this.FinishOrder()) { if (this.orderQueue.length > 0 && this.orderQueue[0].type == "WalkAndFight") this.FindNewTargets(); return; } // See if we can switch to a new nearby enemy if (this.FindNewTargets()) { // Attempt to immediately re-enter the timer function, to avoid wasting the attack. this.TimerHandler(msg.data, msg.lateness); return; } // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); }, // TODO: respond to target deaths immediately, rather than waiting // until the next Timer event "Attacked": function(msg) { if (this.order.data.target != msg.data.attacker) { // If we're attacked by a close enemy, stronger than our current target, // we choose to attack it, but only if we're not forced to target something else if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force))) { var ents = [this.order.data.target, msg.data.attacker]; SortEntitiesByPriority(ents); if (ents[0] != this.order.data.target) { this.RespondToTargetedEntities(ents); } } } }, }, "CHASING": { "enter": function () { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { // Show carried resources when walking. this.SetGathererAnimationOverride(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { this.SetNextState("ATTACKING"); }, }, }, "GATHER": { "APPROACHING": { "enter": function() { this.SelectAnimation("move"); this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". // check that we can gather from the resource we're supposed to gather from. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (!cmpSupply || !cmpSupply.AddGatherer(this.entity)) { // Save the current order's data in case we need it later var oldType = this.order.data.type; var oldTarget = this.order.data.target; var oldTemplate = this.order.data.template; // Try the next queued order if there is any if (this.FinishOrder()) return true; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( ent != oldTarget && ((type.generic == "treasure" && oldType.generic == "treasure") || (type.specific == oldType.specific && (type.specific != "meat" || oldTemplate == template))) ); }); if (nearby) { this.PerformGather(nearby, false, false); return true; } else { // It's probably better in this case, to avoid units getting stuck around a dropsite // in a "Target is far away, full, nearby are no good resources, return to dropsite" loop // to order it to GatherNear the resource position. var cmpPosition = Engine.QueryInterface(this.gatheringTarget, IID_Position); if (cmpPosition) { var pos = cmpPosition.GetPosition(); this.GatherNearPosition(pos.x, pos.z, oldType, oldTemplate); return true; } else { // we're kind of stuck here. Return resource. var nearby = this.FindNearestDropsite(oldType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return true; } } } return true; } return false; }, "MoveCompleted": function(msg) { if (msg.data.error) { // We failed to reach the target // remove us from the list of entities gathering from Resource. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); // Save the current order's data in case we need it later var oldType = this.order.data.type; var oldTarget = this.order.data.target; var oldTemplate = this.order.data.template; // Try the next queued order if there is any if (this.FinishOrder()) return; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( ent != oldTarget && ((type.generic == "treasure" && oldType.generic == "treasure") || (type.specific == oldType.specific && (type.specific != "meat" || oldTemplate == template))) ); }); if (nearby) { this.PerformGather(nearby, false, false); return; } // Couldn't find anything else. Just try this one again, // maybe we'll succeed next time this.PerformGather(oldTarget, false, false); return; } // We reached the target - start gathering from it now this.SetNextState("GATHERING"); }, "leave": function() { var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; }, }, // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function(msg) { var resourceType = this.order.data.type; var resourceTemplate = this.order.data.template; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( (type.generic == "treasure" && resourceType.generic == "treasure") || (type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template)) ); }); // If there is a nearby resource start gathering if (nearby) { this.PerformGather(nearby, false, false); return; } // Couldn't find nearby resources, so give up this.FinishOrder(); }, }, "GATHERING": { "enter": function() { this.gatheringTarget = this.order.data.target; // deleted in "leave". // Check if the resource is full. if (this.gatheringTarget) { // Check that we can gather from the resource we're supposed to gather from. // Will only be added if we're not already in. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (!cmpSupply || !cmpSupply.AddGatherer(this.entity)) { this.gatheringTarget = INVALID_ENTITY; this.StartTimer(0); return false; } } // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) this.order.data.force = false; this.order.data.autoharvest = true; // Calculate timing based on gather rates // This allows the gather rate to control how often we gather, instead of how much. var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget); if (!rate) { // Try to find another target if the current one stopped existing if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity)) { // Let the Timer logic handle this this.StartTimer(0); return false; } // No rate, give up on gathering this.FinishOrder(); return true; } // Scale timing interval based on rate, and start timer // The offset should be at least as long as the repeat time so we use the same value for both. var offset = 1000/rate; var repeat = offset; this.StartTimer(offset, repeat); // We want to start the gather animation as soon as possible, // but only if we're actually at the target and it's still alive // (else it'll look like we're chopping empty air). // (If it's not alive, the Timer handler will deal with sending us // off to a different target.) if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { var typename = "gather_" + this.order.data.type.specific; this.SelectAnimation(typename, false, 1.0, typename); } return false; }, "leave": function() { this.StopTimer(); var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; // Show the carried resource, if we've gathered anything. this.SetGathererAnimationOverride(); }, "Timer": function(msg) { var resourceTemplate = this.order.data.template; var resourceType = this.order.data.type; var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply && cmpSupply.IsAvailable(this.entity)) { // Check we can still reach and gather from the target if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget)) { // Gather the resources: var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); // Try to gather treasure if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget)) return; // If we've already got some resources but they're the wrong type, // drop them first to ensure we're only ever carrying one type if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic)) cmpResourceGatherer.DropResources(); // Collect from the target var status = cmpResourceGatherer.PerformGather(this.gatheringTarget); // If we've collected as many resources as possible, // return to the nearest dropsite if (status.filled) { var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) { // (Keep this Gather order on the stack so we'll // continue gathering after returning) this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // Oh no, couldn't find any drop sites. Give up on gathering. this.FinishOrder(); return; } // We can gather more from this target, do so in the next timer if (!status.exhausted) return; } else { // Try to follow the target if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { this.SetNextState("APPROACHING"); return; } // Can't reach the target, or it doesn't exist any more // We want to carry on gathering resources in the same area as // the old one. So try to get close to the old resource's // last known position var maxRange = 8; // get close but not too close if (this.order.data.lastPos && this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z, 0, maxRange)) { this.SetNextState("APPROACHING"); return; } } } // We're already in range, can't get anywhere near it or the target is exhausted. // Give up on this order and try our next queued order if (this.FinishOrder()) return; // No remaining orders - pick a useful default behaviour // Try to find a new resource of the same specific type near our current position: // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( (type.generic == "treasure" && resourceType.generic == "treasure") || (type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template)) ); }); if (nearby) { this.PerformGather(nearby, false, false); return; } // Nothing else to gather - if we're carrying anything then we should // drop it off, and if not then we might as well head to the dropsite // anyway because that's a nice enough place to congregate and idle var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // No dropsites - just give up }, }, }, "HEAL": { "Attacked": function(msg) { // If we stand ground we will rather die than flee if (!this.GetStance().respondStandGround && !this.order.data.force) this.Flee(msg.data.attacker, false); }, "APPROACHING": { "enter": function () { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { this.SetNextState("HEALING"); }, }, "HEALING": { "enter": function() { var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); this.healTimers = cmpHeal.GetTimers(); // If the repeat time since the last heal hasn't elapsed, // delay the action to avoid healing too fast. var prepare = this.healTimers.prepare; if (this.lastHealed) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } this.SelectAnimation("heal", false, 1.0, "heal"); this.SetAnimationSync(prepare, this.healTimers.repeat); this.StartTimer(prepare, this.healTimers.repeat); // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = (prepare != this.healTimers.prepare) ? true : false; this.FaceTowardsTarget(this.order.data.target); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { var target = this.order.data.target; // Check the target is still alive and healable if (this.TargetIsAlive(target) && this.CanHeal(target)) { // Check if we can still reach the target if (this.CheckTargetRange(target, IID_Heal)) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastHealed = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); cmpHeal.PerformHeal(target); if (this.resyncAnimation) { this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat); this.resyncAnimation = false; } return; } // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetRange(target, IID_Heal)) { this.SetNextState("HEAL.CHASING"); return; } } } // Can't reach it, healed to max hp or doesn't exist any more - give up if (this.FinishOrder()) return; // Heal another one if (this.FindNewHealTargets()) return; // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); }, }, "CHASING": { "enter": function () { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function () { this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function () { this.SetNextState("HEALING"); }, }, }, // Returning to dropsite "RETURNRESOURCE": { "APPROACHING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { // Switch back to idle animation to guarantee we won't // get stuck with the carry animation after stopping moving this.SelectAnimation("idle"); // Check the dropsite is in range and we can return our resource there // (we didn't get stopped before reaching it) if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true)) { var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite); if (cmpResourceDropsite) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); cmpResourceGatherer.CommitResources(dropsiteTypes); // Stop showing the carried resource animation. this.SetGathererAnimationOverride(); // Our next order should always be a Gather, // so just switch back to that order this.FinishOrder(); return; } } // The dropsite was destroyed, or we couldn't reach it, or ownership changed // Look for a new one. var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var genericType = cmpResourceGatherer.GetMainCarryingType(); var nearby = this.FindNearestDropsite(genericType); if (nearby) { this.FinishOrder(); this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // Oh no, couldn't find any drop sites. Give up on returning. this.FinishOrder(); }, }, }, "TRADE": { "Attacked": function(msg) { // Ignore attack // TODO: Inform player }, "APPROACHINGFIRSTMARKET": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "INDIVIDUAL.TRADE.APPROACHINGSECONDMARKET"); }, }, "APPROACHINGSECONDMARKET": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.order.data.firstPass = false; this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET"); }, }, }, "REPAIR": { "APPROACHING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("REPAIRING"); }, }, "REPAIRING": { "enter": function() { // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) if (this.order.data.force) this.order.data.autoharvest = true; this.order.data.force = false; this.repairTarget = this.order.data.target; // temporary, deleted in "leave". // Check we can still reach and repair the target if (!this.CheckTargetRange(this.repairTarget, IID_Builder) || !this.CanRepair(this.repairTarget)) { // Can't reach it, no longer owned by ally, or it doesn't exist any more this.FinishOrder(); return true; } // Check if the target is still repairable var cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints()) { // The building was already finished/fully repaired before we arrived; // let the ConstructionFinished handler handle this. this.OnGlobalConstructionFinished({"entity": this.repairTarget, "newentity": this.repairTarget}); return true; } var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation); if (cmpFoundation) cmpFoundation.AddBuilder(this.entity); this.SelectAnimation("build", false, 1.0, "build"); this.StartTimer(1000, 1000); return false; }, "leave": function() { var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation); if (cmpFoundation) cmpFoundation.RemoveBuilder(this.entity); delete this.repairTarget; this.StopTimer(); }, "Timer": function(msg) { // Check we can still reach and repair the target if (!this.CheckTargetRange(this.repairTarget, IID_Builder) || !this.CanRepair(this.repairTarget)) { // Can't reach it, no longer owned by ally, or it doesn't exist any more this.FinishOrder(); return; } var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); cmpBuilder.PerformBuilding(this.repairTarget); }, }, "ConstructionFinished": function(msg) { if (msg.data.entity != this.order.data.target) return; // ignore other buildings // Save the current order's data in case we need it later var oldData = this.order.data; // Save the current state so we can continue walking if necessary // FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation. // Idle animation while moving towards finished construction looks weird (ghosty). var oldState = this.GetCurrentState(); // We finished building it. // Switch to the next order (if any) if (this.FinishOrder()) return; // No remaining orders - pick a useful default behaviour // If autocontinue explicitly disabled (e.g. by AI) then // do nothing automatically if (!oldData.autocontinue) return; // If this building was e.g. a farm of ours, the entities that recieved // the build command should start gathering from it if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity)) { this.PerformGather(msg.data.newentity, true, false); return; } // If this building was e.g. a farmstead of ours, entities that received // the build command should look for nearby resources to gather if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false)) { var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); var types = cmpResourceDropsite.GetTypes(); // TODO: Slightly undefined behavior here, we don't know what type of resource will be collected, // may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that! var nearby = this.FindNearbyResource(function (ent, type, template) { return (types.indexOf(type.generic) != -1); }); if (nearby) { this.PerformGather(nearby, true, false); return; } } // Look for a nearby foundation to help with var nearbyFoundation = this.FindNearbyFoundation(); if (nearbyFoundation) { this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true); return; } // Unit was approaching and there's nothing to do now, so switch to walking if (oldState === "INDIVIDUAL.REPAIR.APPROACHING") { // We're already walking to the given point, so add this as a order. this.WalkToTarget(msg.data.newentity, true); } }, }, "GARRISON": { "APPROACHING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("GARRISONED"); }, "leave": function() { this.StopTimer(); } }, "GARRISONED": { "enter": function() { var target = this.order.data.target; var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); // Check that we can garrison here if (this.CanGarrison(target)) { // Check that we're in range of the garrison target if (this.CheckGarrisonRange(target)) { // Check that garrisoning succeeds if (cmpGarrisonHolder.Garrison(this.entity)) { this.isGarrisoned = true; // Check if we are garrisoned in a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (cmpResourceDropsite) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) { cmpResourceGatherer.CommitResources(dropsiteTypes); this.SetGathererAnimationOverride(); } } return false; } } else { // Unable to reach the target, try again // (or follow if it's a moving target) if (this.MoveToTarget(target)) { this.SetNextState("APPROACHING"); return false; } } } // Garrisoning failed for some reason, so finish the order this.FinishOrder(); return true; }, "Order.Ungarrison": function() { if (this.FinishOrder()) return; }, "leave": function() { this.isGarrisoned = false; } }, }, "CHEERING": { "enter": function() { // Unit is invulnerable while cheering var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(true); this.SelectAnimation("promotion"); this.StartTimer(2800, 2800); return false; }, "leave": function() { this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(false); }, "Timer": function(msg) { this.FinishOrder(); }, }, "PACKING": { "enter": function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Pack(); }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { }, "Attacked": function(msg) { // Ignore attacks while packing }, }, "UNPACKING": { "enter": function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Unpack(); }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { }, "Attacked": function(msg) { // Ignore attacks while unpacking }, }, }, "ANIMAL": { "Attacked": function(msg) { if (this.template.NaturalBehaviour == "skittish" || this.template.NaturalBehaviour == "passive") { this.Flee(msg.data.attacker, false); } else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive") { if (this.CanAttack(msg.data.attacker)) this.Attack(msg.data.attacker, false); } else if (this.template.NaturalBehaviour == "domestic") { // Never flee, stop what we were doing this.SetNextState("IDLE"); } }, "Order.LeaveFoundation": function(msg) { // Run away from the foundation this.MoveToTargetRangeExplicit(msg.data.target, +this.template.FleeDistance, +this.template.FleeDistance); this.SetNextState("FLEEING"); }, "IDLE": { // (We need an IDLE state so that FinishOrder works) "enter": function() { // Start feeding immediately this.SetNextState("FEEDING"); return true; }, }, "ROAMING": { "enter": function() { // Walk in a random direction this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); }, "leave": function() { this.StopTimer(); }, "LosRangeUpdate": function(msg) { if (this.template.NaturalBehaviour == "skittish") { if (msg.data.added.length > 0) { this.Flee(msg.data.added[0], false); return; } } // Start attacking one of the newly-seen enemy (if any) else if (this.IsDangerousAnimal()) { this.AttackVisibleEntity(msg.data.added); } // TODO: if two units enter our range together, we'll attack the // first and then the second won't trigger another LosRangeUpdate // so we won't notice it. Probably we should do something with // ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to // find any units that are already in range. }, "Timer": function(msg) { this.SetNextState("FEEDING"); }, "MoveCompleted": function() { this.MoveRandomly(+this.template.RoamDistance); }, }, "FEEDING": { "enter": function() { // Stop and eat for a while this.SelectAnimation("feeding"); this.StopMoving(); this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax)); }, "leave": function() { this.StopTimer(); }, "LosRangeUpdate": function(msg) { if (this.template.NaturalBehaviour == "skittish") { if (msg.data.added.length > 0) { this.Flee(msg.data.added[0], false); return; } } // Start attacking one of the newly-seen enemy (if any) else if (this.template.NaturalBehaviour == "violent") { this.AttackVisibleEntity(msg.data.added); } }, "MoveCompleted": function() { }, "Timer": function(msg) { this.SetNextState("ROAMING"); }, }, "FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals "COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals "WALKING": "INDIVIDUAL.WALKING", // reuse the same walking behaviour for animals // only used for domestic animals }, }; var UnitFsm = new FSM(UnitFsmSpec); UnitAI.prototype.Init = function() { this.orderQueue = []; // current order is at the front of the list this.order = undefined; // always == this.orderQueue[0] this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to this.isGarrisoned = false; this.isIdle = false; this.lastFormationName = ""; this.finishedOrder = false; // used to find if all formation members finished the order // For preventing increased action rate due to Stop orders or target death. this.lastAttacked = undefined; this.lastHealed = undefined; this.SetStance(this.template.DefaultStance); }; UnitAI.prototype.IsFormationController = function() { return (this.template.FormationController == "true"); }; UnitAI.prototype.IsFormationMember = function() { return (this.formationController != INVALID_ENTITY); }; UnitAI.prototype.HasFinishedOrder = function() { return this.finishedOrder; }; UnitAI.prototype.ResetFinishOrder = function() { this.finishedOrder = false; }; UnitAI.prototype.IsAnimal = function() { return (this.template.NaturalBehaviour ? true : false); }; UnitAI.prototype.IsDangerousAnimal = function() { return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" || this.template.NaturalBehaviour == "aggressive")); }; UnitAI.prototype.IsDomestic = function() { var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (!cmpIdentity) return false; return cmpIdentity.HasClass("Domestic"); }; UnitAI.prototype.IsHealer = function() { return Engine.QueryInterface(this.entity, IID_Heal); }; UnitAI.prototype.IsIdle = function() { return this.isIdle; }; UnitAI.prototype.IsGarrisoned = function() { return this.isGarrisoned; }; UnitAI.prototype.IsWalking = function() { var state = this.GetCurrentState().split(".").pop(); return (state == "WALKING"); }; UnitAI.prototype.CanAttackGaia = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; // Rejects Gaia (0) and INVALID_PLAYER (-1) var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() <= 0) return false; return true; }; UnitAI.prototype.OnCreate = function() { if (this.IsAnimal()) UnitFsm.Init(this, "ANIMAL.FEEDING"); else if (this.IsFormationController()) UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE"); else UnitFsm.Init(this, "INDIVIDUAL.IDLE"); }; UnitAI.prototype.OnDiplomacyChanged = function(msg) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) this.SetupRangeQuery(); }; UnitAI.prototype.OnOwnershipChanged = function(msg) { this.SetupRangeQueries(); // If the unit isn't being created or dying, clear orders and reset stance. if (msg.to != -1 && msg.from != -1) { this.SetStance(this.template.DefaultStance); this.Stop(false); } }; UnitAI.prototype.OnDestroy = function() { // Switch to an empty state to let states execute their leave handlers. UnitFsm.SwitchToNextState(this, ""); // Clean up range queries var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) rangeMan.DestroyActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) rangeMan.DestroyActiveQuery(this.losHealRangeQuery); if (this.losGaiaRangeQuery) rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery); }; UnitAI.prototype.OnVisionRangeChanged = function(msg) { // Update range queries if (this.entity == msg.entity) this.SetupRangeQueries(); }; // Wrapper function that sets up the normal, healer, and Gaia range queries. UnitAI.prototype.SetupRangeQueries = function() { this.SetupRangeQuery(); if (this.IsHealer()) this.SetupHealRangeQuery(); if (this.CanAttackGaia() || this.losGaiaRangeQuery) this.SetupGaiaRangeQuery(); } // Set up a range query for all enemy units within LOS range // which can be attacked. // This should be called whenever our ownership changes. UnitAI.prototype.SetupRangeQuery = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.losRangeQuery) { rangeMan.DestroyActiveQuery(this.losRangeQuery); this.losRangeQuery = undefined; } var players = []; if (owner != -1) { // If unit not just killed, get enemy players via diplomacy var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); var numPlayers = playerMan.GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) { // Exclude gaia, allies, and self // TODO: How to handle neutral players - Special query to attack military only? if (cmpPlayer.IsEnemy(i)) players.push(i); } } var range = this.GetQueryRange(IID_Attack); this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, rangeMan.GetEntityFlagMask("normal")); rangeMan.EnableActiveQuery(this.losRangeQuery); }; // Set up a range query for all own or ally units within LOS range // which can be healed. // This should be called whenever our ownership changes. UnitAI.prototype.SetupHealRangeQuery = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.losHealRangeQuery) rangeMan.DestroyActiveQuery(this.losHealRangeQuery); var players = [owner]; if (owner != -1) { // If unit not just killed, get ally players via diplomacy var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); var numPlayers = playerMan.GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) { // Exclude gaia and enemies if (cmpPlayer.IsAlly(i)) players.push(i); } } var range = this.GetQueryRange(IID_Heal); this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, rangeMan.GetEntityFlagMask("injured")); rangeMan.EnableActiveQuery(this.losHealRangeQuery); }; // Set up a range query for Gaia units within LOS range which can be attacked. // This should be called whenever our ownership changes. UnitAI.prototype.SetupGaiaRangeQuery = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.losGaiaRangeQuery) { rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery); this.losGaiaRangeQuery = undefined; } // Only create the query if Gaia is our enemy and we can attack. if (this.CanAttackGaia()) { var range = this.GetQueryRange(IID_Attack); // This query is only interested in Gaia entities that can attack. this.losGaiaRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal")); rangeMan.EnableActiveQuery(this.losGaiaRangeQuery); } }; //// FSM linkage functions //// UnitAI.prototype.SetNextState = function(state) { UnitFsm.SetNextState(this, state); }; // This will make sure that the state is always entered even if this means leaving it and reentering it // This is so that a state can be reinitialized with new order data without having to switch to an intermediate state UnitAI.prototype.SetNextStateAlwaysEntering = function(state) { UnitFsm.SetNextStateAlwaysEntering(this, state); }; UnitAI.prototype.DeferMessage = function(msg) { UnitFsm.DeferMessage(this, msg); }; UnitAI.prototype.GetCurrentState = function() { return UnitFsm.GetCurrentState(this); }; UnitAI.prototype.FsmStateNameChanged = function(state) { Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state }); }; /** * Call when the current order has been completed (or failed). * Removes the current order from the queue, and processes the * next one (if any). Returns false and defaults to IDLE * if there are no remaining orders. */ UnitAI.prototype.FinishOrder = function() { if (!this.orderQueue.length) { var stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(this.entity); error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack); } this.orderQueue.shift(); this.order = this.orderQueue[0]; if (this.orderQueue.length) { var ret = UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // If the order was rejected then immediately take it off // and process the remaining queue if (ret && ret.discardOrder) { return this.FinishOrder(); } // Otherwise we've successfully processed a new order return true; } else { this.SetNextState("IDLE"); // Check if there are queued formation orders if (this.IsFormationMember()) { var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpUnitAI) { // Inform the formation controller that we finished this task this.finishedOrder = true; // We don't want to carry out the default order // if there are still queued formation orders left if (cmpUnitAI.GetOrders().length > 1) return true; } } return false; } }; /** * Add an order onto the back of the queue, * and execute it if we didn't already have an order. */ UnitAI.prototype.PushOrder = function(type, data) { var order = { "type": type, "data": data }; this.orderQueue.push(order); // If we didn't already have an order, then process this new one if (this.orderQueue.length == 1) { this.order = order; var ret = UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // If the order was rejected then immediately take it off // and process the remaining queue if (ret && ret.discardOrder) { this.FinishOrder(); } } }; /** * Add an order onto the front of the queue, * and execute it immediately. */ UnitAI.prototype.PushOrderFront = function(type, data) { var order = { "type": type, "data": data }; // If current order is cheering then add new order after it if (this.order && this.order.type == "Cheering") { var cheeringOrder = this.orderQueue.shift(); this.orderQueue.unshift(cheeringOrder, order); } else { this.orderQueue.unshift(order); this.order = order; var ret = UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // If the order was rejected then immediately take it off again; // assume the previous active order is still valid (the short-lived // new order hasn't changed state or anything) so we can carry on // as if nothing had happened if (ret && ret.discardOrder) { this.orderQueue.shift(); this.order = this.orderQueue[0]; } } }; UnitAI.prototype.ReplaceOrder = function(type, data) { // Special cases of orders that shouldn't be replaced: // 1. Cheering - we're invulnerable, add order after we finish // 2. Packing/unpacking - we're immobile, add order after we finish (unless it's cancel) // TODO: maybe a better way of doing this would be to use priority levels if (this.order && this.order.type == "Cheering") { var order = { "type": type, "data": data }; var cheeringOrder = this.orderQueue.shift(); this.orderQueue = [ cheeringOrder, order ]; } else if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack") { var order = { "type": type, "data": data }; var packingOrder = this.orderQueue.shift(); this.orderQueue = [ packingOrder, order ]; } else { this.orderQueue = []; this.PushOrder(type, data); } }; UnitAI.prototype.GetOrders = function() { return this.orderQueue.slice(); }; UnitAI.prototype.AddOrders = function(orders) { for each (var order in orders) { this.PushOrder(order.type, order.data); } }; UnitAI.prototype.GetOrderData = function() { var orders = []; for (var i in this.orderQueue) { if (this.orderQueue[i].data) orders.push(deepcopy(this.orderQueue[i].data)); } return orders; }; UnitAI.prototype.TimerHandler = function(data, lateness) { // Reset the timer if (data.timerRepeat === undefined) { this.timer = undefined; } UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness}); }; /** * Set up the UnitAI timer to run after 'offset' msecs, and then * every 'repeat' msecs until StopTimer is called. A "Timer" message * will be sent each time the timer runs. */ UnitAI.prototype.StartTimer = function(offset, repeat) { if (this.timer) error("Called StartTimer when there's already an active timer"); var data = { "timerRepeat": repeat }; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (repeat === undefined) this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data); else this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data); }; /** * Stop the current UnitAI timer. */ UnitAI.prototype.StopTimer = function() { if (!this.timer) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; //// Message handlers ///// UnitAI.prototype.OnMotionChanged = function(msg) { if (msg.starting && !msg.error) { UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); } else if (!msg.starting || msg.error) { UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); } }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) { // TODO: This is a bit inefficient since every unit listens to every // construction message - ideally we could scope it to only the one we're building UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg}); }; UnitAI.prototype.OnGlobalEntityRenamed = function(msg) { for each (var order in this.orderQueue) if (order.data && order.data.target && order.data.target == msg.entity) order.data.target = msg.newentity; }; UnitAI.prototype.OnAttacked = function(msg) { UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg}); }; UnitAI.prototype.OnHealthChanged = function(msg) { UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to}); }; UnitAI.prototype.OnRangeUpdate = function(msg) { if (msg.tag == this.losRangeQuery) UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg}); else if (msg.tag == this.losGaiaRangeQuery) UnitFsm.ProcessMessage(this, {"type": "LosGaiaRangeUpdate", "data": msg}); else if (msg.tag == this.losHealRangeQuery) UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg}); }; UnitAI.prototype.OnPackFinished = function(msg) { UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed}); }; //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.GetWalkSpeed(); }; UnitAI.prototype.GetRunSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.GetRunSpeed(); }; /** * Returns true if the target exists and has non-zero hitpoints. */ UnitAI.prototype.TargetIsAlive = function(ent) { var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (!cmpHealth) return false; return (cmpHealth.GetHitpoints() != 0); }; /** * Returns true if the target exists and needs to be killed before * beginning to gather resources from it. */ UnitAI.prototype.MustKillGatherTarget = function(ent) { var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (!cmpResourceSupply) return false; if (!cmpResourceSupply.GetKillBeforeGather()) return false; return this.TargetIsAlive(ent); }; /** * Returns the entity ID of the nearest resource supply where the given * filter returns true, or undefined if none can be found. * TODO: extend this to exclude resources that already have lots of * gatherers. */ UnitAI.prototype.FindNearbyResource = function(filter) { var range = 64; // TODO: what's a sensible number? var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); // We accept resources owned by Gaia or any player var players = [0]; for (var i = 1; i < playerMan.GetNumPlayers(); ++i) players.push(i); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply); for each (var ent in nearby) { var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); var type = cmpResourceSupply.GetType(); var amount = cmpResourceSupply.GetCurrentAmount(); var template = cmpTemplateManager.GetCurrentTemplateName(ent); // Remove "resource|" prefix from template names, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); if (amount > 0 && cmpResourceSupply.IsAvailable(this.entity) && filter(ent, type, template)) return ent; } return undefined; }; /** * Returns the entity ID of the nearest resource dropsite that accepts * the given type, or undefined if none can be found. */ UnitAI.prototype.FindNearestDropsite = function(genericType) { // Find dropsites owned by this unit's player var players = []; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) players.push(cmpOwnership.GetOwner()); // Ships are unable to reach land dropsites and shouldn't attempt to do so. var excludeLand = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship"); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite); if (excludeLand) { nearby = nearby.filter( function(e) { return Engine.QueryInterface(e, IID_Identity).HasClass("Naval"); }); } for each (var ent in nearby) { var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (!cmpDropsite.AcceptsType(genericType)) continue; return ent; } return undefined; }; /** * Returns the entity ID of the nearest building that needs to be constructed, * or undefined if none can be found close enough. */ UnitAI.prototype.FindNearbyFoundation = function() { var range = 64; // TODO: what's a sensible number? // Find buildings owned by this unit's player var players = []; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) players.push(cmpOwnership.GetOwner()); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_Foundation); for each (var ent in nearby) { // Skip foundations that are already complete. (This matters since // we process the ConstructionFinished message before the foundation // we're working on has been deleted.) var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation.IsFinished()) continue; return ent; } return undefined; }; /** * Play a sound appropriate to the current entity. */ UnitAI.prototype.PlaySound = function(name) { // If we're a formation controller, use the sounds from our first member if (this.IsFormationController()) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); var member = cmpFormation.GetPrimaryMember(); if (member) PlaySound(name, member); } else { // Otherwise use our own sounds PlaySound(name, this.entity); } }; UnitAI.prototype.SetGathererAnimationOverride = function(disable) { var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return; var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; // Remove the animation override, so that weapons are shown again. if (disable) { cmpVisual.ResetMoveAnimation("walk"); return; } // Work out what we're carrying, in order to select an appropriate animation var type = cmpResourceGatherer.GetLastCarriedType(); if (type) { var typename = "carry_" + type.generic; // Special case for meat if (type.specific == "meat") typename = "carry_" + type.specific; cmpVisual.ReplaceMoveAnimation("walk", typename); } else cmpVisual.ResetMoveAnimation("walk"); } UnitAI.prototype.SelectAnimation = function(name, once, speed, sound) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; // Special case: the "move" animation gets turned into a special // movement mode that deals with speeds and walk/run automatically if (name == "move") { // Speed to switch from walking to running animations var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2; cmpVisual.SelectMovementAnimation(runThreshold); return; } var soundgroup; if (sound) { var cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (cmpSound) soundgroup = cmpSound.GetSoundGroup(sound); } // Set default values if unspecified if (once === undefined) once = false; if (speed === undefined) speed = 1.0; if (soundgroup === undefined) soundgroup = ""; cmpVisual.SelectAnimation(name, once, speed, soundgroup); }; UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetAnimationSyncRepeat(repeattime); cmpVisual.SetAnimationSyncOffset(actiontime); }; UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.StopMoving(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; UnitAI.prototype.MoveToTarget = function(target) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, 0, 0); }; UnitAI.prototype.MoveToTargetRange = function(target, iid, type) { if (!this.CheckTargetVisible(target)) return false; var cmpRanged = Engine.QueryInterface(this.entity, iid); var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; +/** + * Move unit so we hope the target is in the attack range + * for melee attacks, this goes straight to the default range checks + * for ranged attacks, the parabolic range is used, so we can't know exactly at what horizontal range the target can be reached + * That's why a guess is needed + * a guess of 1 will take the maximum of the possible ranges, and stay far away + * a guess of 0 will take the minimum of the possible ranges and, in most cases, will have the target in range. + * every guess inbetween is a linear interpollation + */ +UnitAI.prototype.MoveToTargetAttackRange = function(target, iid, type,guess) +{ + + if(type!= "Ranged") + return this.MoveToTargetRange(target, iid, type); + + if (!this.CheckTargetVisible(target)) + return false; + + var cmpRanged = Engine.QueryInterface(this.entity, iid); + var range = cmpRanged.GetRange(type); + + var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); + var s = thisCmpPosition.GetPosition(); + + var targetCmpPosition = Engine.QueryInterface(target, IID_Position); + if(!targetCmpPosition.IsInWorld()) + return false; + + var t = targetCmpPosition.GetPosition(); + // h is positive when I'm higher than the target + var h = s.y-t.y+range.elevationBonus; + + // No negative roots please + if(h>-range.max/2) + var parabolicMaxRange = Math.sqrt(range.max*range.max+2*range.max*h); + else + // return false? Or hope you come close enough? + var parabolicMaxRange = 0; + //return false; + + // the parabole changes while walking, take something in the middle + var guessedMaxRange = Math.max(range.max, parabolicMaxRange)*guess+Math.min(range.max, parabolicMaxRange)*(1-guess) ; + + var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + return cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange); +}; + UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, min, max); }; UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInPointRange(x, z, min, max); }; UnitAI.prototype.CheckTargetRange = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); }; +/** + * Check if the target is inside the attack range + * For melee attacks, this goes straigt to the regular range calculation + * For ranged attacks, the parabolic formula is used to accout for bigger ranges + * when the target is lower, and smaller ranges when the target is higher + */ +UnitAI.prototype.CheckTargetAttackRange = function(target, iid, type) +{ + + if (type != "Ranged") + return this.CheckTargetRange(target,iid,type); + + var targetCmpPosition = Engine.QueryInterface(target, IID_Position); + if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) + return false; + + var cmpRanged = Engine.QueryInterface(this.entity, iid); + var range = cmpRanged.GetRange(type); + + var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); + var s = thisCmpPosition.GetPosition(); + + var t = targetCmpPosition.GetPosition(); + + var h = s.y-t.y+range.elevationBonus; + var maxRangeSq = 2*range.max*(h + range.max/2); + + if (maxRangeSq < 0) + return false; + + var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq)); + + return maxRangeSq >= distanceSq && range.min*range.min <= distanceSq; + +}; + UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, min, max); }; UnitAI.prototype.CheckGarrisonRange = function(target) { var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); var range = cmpGarrisonHolder.GetLoadingRange(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); }; /** * Returns true if the target entity is visible through the FoW/SoD. */ UnitAI.prototype.CheckTargetVisible = function(target) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return false; if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden") return false; // Either visible directly, or visible in fog return true; }; UnitAI.prototype.FaceTowardsTarget = function(target) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); var targetpos = cmpTargetPosition.GetPosition(); var angle = Math.atan2(targetpos.x - pos.x, targetpos.z - pos.z); var rot = cmpPosition.GetRotation(); var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI; if (Math.abs(delta) > 0.2) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.z); } }; UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type); var cmpPosition = Engine.QueryInterface(target, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; var halfvision = cmpVision.GetRange() / 2; var pos = cmpPosition.GetPosition(); var heldPosition = this.heldPosition; if (heldPosition === undefined) heldPosition = {"x": pos.x, "z": pos.z}; var dx = heldPosition.x - pos.x; var dz = heldPosition.z - pos.z; var dist = Math.sqrt(dx*dx + dz*dz); return dist < halfvision + range.max; }; UnitAI.prototype.CheckTargetIsInVisionRange = function(target) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; var range = cmpVision.GetRange(); var distance = DistanceBetweenEntities(this.entity,target); return distance < range; }; UnitAI.prototype.GetBestAttack = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; return cmpAttack.GetBestAttack(); }; UnitAI.prototype.GetBestAttackAgainst = function(target) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; return cmpAttack.GetBestAttackAgainst(target); }; UnitAI.prototype.GetAttackBonus = function(type, target) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return 1; return cmpAttack.GetAttackBonus(type, target); }; /** * Try to find one of the given entities which can be attacked, * and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackVisibleEntity = function(ents, forceResponse) { for each (var target in ents) { if (this.CanAttack(target, forceResponse)) { this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse }); return true; } } return false; }; /** * Try to find one of the given entities which can be attacked * and which is close to the hold position, and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackEntityInZone = function(ents, forceResponse) { for each (var target in ents) { var type = this.GetBestAttackAgainst(target); if (this.CanAttack(target, forceResponse) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, type) && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))) { this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse }); return true; } } return false; }; /** * Try to respond appropriately given our current stance, * given a list of entities that match our stance's target criteria. * Returns true if it responded. */ UnitAI.prototype.RespondToTargetedEntities = function(ents) { if (!ents.length) return false; if (this.GetStance().respondChase) return this.AttackVisibleEntity(ents, true); if (this.GetStance().respondStandGround) return this.AttackVisibleEntity(ents, true); if (this.GetStance().respondHoldGround) return this.AttackEntityInZone(ents, true); if (this.GetStance().respondFlee) { this.PushOrderFront("Flee", { "target": ents[0], "force": false }); return true; } return false; }; /** * Try to respond to healable entities. * Returns true if it responded. */ UnitAI.prototype.RespondToHealableEntities = function(ents) { if (!ents.length) return false; for each (var ent in ents) { if (this.CanHeal(ent)) { this.PushOrderFront("Heal", { "target": ent, "force": false }); return true; } } return false; }; /** * Returns true if we should stop following the target entity. */ UnitAI.prototype.ShouldAbandonChase = function(target, force, iid) { // Forced orders shouldn't be interrupted. if (force) return false; // Stop if we're in hold-ground mode and it's too far from the holding point if (this.GetStance().respondHoldGround) { if (!this.CheckTargetDistanceFromHeldPosition(target, iid, this.order.data.attackType)) return true; } // Stop if it's left our vision range, unless we're especially persistent if (!this.GetStance().respondChaseBeyondVision) { if (!this.CheckTargetIsInVisionRange(target)) return true; } // (Note that CCmpUnitMotion will detect if the target is lost in FoW, // and will continue moving to its last seen position and then stop) return false; }; /* * Returns whether we should chase the targeted entity, * given our current stance. */ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force) { // TODO: use special stances instead? var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack) return false; if (this.GetStance().respondChase) return true; if (force) return true; return false; }; //// External interface functions //// UnitAI.prototype.SetFormationController = function(ent) { this.formationController = ent; // Set obstruction group, so we can walk through members // of our own formation (or ourself if not in formation) var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) { if (ent == INVALID_ENTITY) cmpObstruction.SetControlGroup(this.entity); else cmpObstruction.SetControlGroup(ent); } // If we were removed from a formation, let the FSM switch back to INDIVIDUAL if (ent == INVALID_ENTITY) UnitFsm.ProcessMessage(this, { "type": "FormationLeave" }); }; UnitAI.prototype.GetFormationController = function() { return this.formationController; }; UnitAI.prototype.SetLastFormationName = function(name) { this.lastFormationName = name; }; UnitAI.prototype.GetLastFormationName = function() { return this.lastFormationName; }; UnitAI.prototype.MoveIntoFormation = function(cmd) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; cmpFormation.LoadFormation(cmd.name); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); // Add new order to move into formation at the current position this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true }); }; /** * Returns the estimated distance that this unit will travel before either * finishing all of its orders, or reaching a non-walk target (attack, gather, etc). * Intended for Formation to switch to column layout on long walks. */ UnitAI.prototype.ComputeWalkingDistance = function() { var distance = 0; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return 0; // Keep track of the position at the start of each order var pos = cmpPosition.GetPosition(); for (var i = 0; i < this.orderQueue.length; ++i) { var order = this.orderQueue[i]; switch (order.type) { case "Walk": case "WalkAndFight": case "WalkToPointRange": case "MoveIntoFormation": case "GatherNearPosition": // Add the distance to the target point var dx = order.data.x - pos.x; var dz = order.data.z - pos.z; var d = Math.sqrt(dx*dx + dz*dz); distance += d; // Remember this as the start position for the next order pos = order.data; break; // and continue the loop case "WalkToTarget": case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will. case "Flee": case "LeaveFoundation": case "Attack": case "Heal": case "Gather": case "ReturnResource": case "Repair": case "Garrison": // Find the target unit's position var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return distance; var targetPos = cmpTargetPosition.GetPosition(); // Add the distance to the target unit var dx = targetPos.x - pos.x; var dz = targetPos.z - pos.z; var d = Math.sqrt(dx*dx + dz*dz); distance += d; // Return the total distance to the target return distance; case "Stop": return 0; default: error("ComputeWalkingDistance: Unrecognised order type '"+order.type+"'"); return distance; } } // Return the total distance to the end of the order queue return distance; }; UnitAI.prototype.AddOrder = function(type, data, queued) { if (queued) this.PushOrder(type, data); else this.ReplaceOrder(type, data); }; /** * Adds walk order to queue, forced by the player. */ UnitAI.prototype.Walk = function(x, z, queued) { this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued); }; /** * Adds stop order to queue, forced by the player. */ UnitAI.prototype.Stop = function(queued) { this.AddOrder("Stop", undefined, queued); }; /** * Adds walk-to-target order to queue, this only occurs in response * to a player order, and so is forced. */ UnitAI.prototype.WalkToTarget = function(target, queued) { this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued); }; /** * Adds walk-and-fight order to queue, this only occurs in response * to a player order, and so is forced. */ UnitAI.prototype.WalkAndFight = function(x, z, queued) { this.AddOrder("WalkAndFight", { "x": x, "z": z, "force": true }, queued); }; /** * Adds leave foundation order to queue, treated as forced. */ UnitAI.prototype.LeaveFoundation = function(target) { // If we're already being told to leave a foundation, then // ignore this new request so we don't end up being too indecisive // to ever actually move anywhere if (this.order && this.order.type == "LeaveFoundation") return; this.PushOrderFront("LeaveFoundation", { "target": target, "force": true }); }; /** * Adds attack order to the queue, forced by the player. */ UnitAI.prototype.Attack = function(target, queued) { if (!this.CanAttack(target)) { // We don't want to let healers walk to the target unit so they can be easily killed. // Instead we just let them get into healing range. if (this.IsHealer()) this.MoveToTargetRange(target, IID_Heal); else this.WalkToTarget(target, queued); return; } this.AddOrder("Attack", { "target": target, "force": true }, queued); }; /** * Adds garrison order to the queue, forced by the player. */ UnitAI.prototype.Garrison = function(target, queued) { if (!this.CanGarrison(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Garrison", { "target": target, "force": true }, queued); }; /** * Adds ungarrison order to the queue. */ UnitAI.prototype.Ungarrison = function() { if (this.IsGarrisoned()) { this.AddOrder("Ungarrison", null, false); } }; /** * Adds gather order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Gather = function(target, queued) { this.PerformGather(target, queued, true); }; /** * Internal function to abstract the force parameter. */ UnitAI.prototype.PerformGather = function(target, queued, force) { if (!this.CanGather(target)) { this.WalkToTarget(target, queued); return; } // Save the resource type now, so if the resource gets destroyed // before we process the order then we still know what resource // type to look for more of var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); var type = cmpResourceSupply.GetType(); // Also save the target entity's template, so that if it's an animal, // we won't go from hunting slow safe animals to dangerous fast ones var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(target); // Remove "resource|" prefix from template name, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); // Remember the position of our target, if any, in case it disappears // later and we want to head to its last known position // (TODO: if the target moves a lot (e.g. it's an animal), maybe we // need to update this lastPos regularly rather than just here?) var lastPos = undefined; var cmpPosition = Engine.QueryInterface(target, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) lastPos = cmpPosition.GetPosition(); this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued); }; /** * Adds gather-near-position order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued) { // Remove "resource|" prefix from template name, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued); }; /** * Adds heal order to the queue, forced by the player. */ UnitAI.prototype.Heal = function(target, queued) { if (!this.CanHeal(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Heal", { "target": target, "force": true }, queued); }; /** * Adds return resource order to the queue, forced by the player. */ UnitAI.prototype.ReturnResource = function(target, queued) { if (!this.CanReturnResource(target, true)) { this.WalkToTarget(target, queued); return; } this.AddOrder("ReturnResource", { "target": target, "force": true }, queued); }; /** * Adds trade order to the queue. Either walk to the first market, or * start a new route. Not forced, so it can be interrupted by attacks. */ UnitAI.prototype.SetupTradeRoute = function(target, source, queued) { if (!this.CanTrade(target)) { this.WalkToTarget(target, queued); return; } var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); var marketsChanged = cmpTrader.SetTargetMarket(target, source); if (marketsChanged) { if (cmpTrader.HasBothMarkets()) this.AddOrder("Trade", { "firstMarket": cmpTrader.GetFirstMarket(), "secondMarket": cmpTrader.GetSecondMarket(), "force": false }, queued); else this.WalkToTarget(cmpTrader.GetFirstMarket(), queued); } }; UnitAI.prototype.MoveToMarket = function(targetMarket) { if (this.MoveToTarget(targetMarket)) { // We've started walking to the market return true; } else { // We can't reach the market. // Give up. this.StopMoving(); this.StopTrading(); return false; } }; UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket, nextMarket, nextFsmStateName) { if (!this.CanTrade(currentMarket)) { this.StopTrading(); return; } if (this.CheckTargetRange(currentMarket, IID_Trader)) { this.PerformTrade(); if (this.MoveToMarket(nextMarket)) { // We've started walking to the next market this.SetNextState(nextFsmStateName); } } else { // If the current market is not reached try again this.MoveToMarket(currentMarket); } }; UnitAI.prototype.PerformTrade = function() { var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); cmpTrader.PerformTrade(); }; UnitAI.prototype.StopTrading = function() { this.FinishOrder(); var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); cmpTrader.StopTrading(); }; /** * Adds repair/build order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Repair = function(target, autocontinue, queued) { if (!this.CanRepair(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued); }; /** * Adds flee order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.Flee = function(target, queued) { this.AddOrder("Flee", { "target": target, "force": false }, queued); }; /** * Adds cheer order to the queue. Forced so it won't be interrupted by attacks. */ UnitAI.prototype.Cheer = function() { this.AddOrder("Cheering", { "force": true }, false); }; UnitAI.prototype.Pack = function(queued) { // Check that we can pack if (this.CanPack()) this.AddOrder("Pack", { "force": true }, queued); }; UnitAI.prototype.Unpack = function(queued) { // Check that we can unpack if (this.CanUnpack()) this.AddOrder("Unpack", { "force": true }, queued); }; UnitAI.prototype.CancelPack = function(queued) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) this.AddOrder("CancelPack", { "force": true }, queued); }; UnitAI.prototype.CancelUnpack = function(queued) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) this.AddOrder("CancelUnpack", { "force": true }, queued); }; UnitAI.prototype.SetStance = function(stance) { if (g_Stances[stance]) this.stance = stance; else error("UnitAI: Setting to invalid stance '"+stance+"'"); }; UnitAI.prototype.SwitchToStance = function(stance) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.SetHeldPosition(pos.x, pos.z); this.SetStance(stance); // Stop moving if switching to stand ground // TODO: Also stop existing orders in a sensible way if (stance == "standground") this.StopMoving(); // Reset the range queries, since the range depends on stance. this.SetupRangeQueries(); }; /** * Resets losRangeQuery, and if there are some targets in range that we can * attack then we start attacking and this returns true; otherwise, returns false. */ UnitAI.prototype.FindNewTargets = function() { if (!this.losRangeQuery) return false; if (!this.GetStance().targetVisibleEnemies) return false; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.AttackEntitiesByPreference( rangeMan.ResetActiveQuery(this.losRangeQuery) )) return true; // If no regular enemies were found, attempt to attack a hostile Gaia entity. else if (this.losGaiaRangeQuery) return this.AttackGaiaEntitiesByPreference( rangeMan.ResetActiveQuery(this.losGaiaRangeQuery) ); return false; }; /** * Resets losHealRangeQuery, and if there are some targets in range that we can heal * then we start healing and this returns true; otherwise, returns false. */ UnitAI.prototype.FindNewHealTargets = function() { if (!this.losHealRangeQuery) return false; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery); for each (var ent in ents) { if (this.CanHeal(ent)) { this.PushOrderFront("Heal", { "target": ent, "force": false }); return true; } } // We haven't found any target to heal return false; }; UnitAI.prototype.GetQueryRange = function(iid) { var ret = { "min": 0, "max": 0 }; if (this.GetStance().respondStandGround) { var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return ret; var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack()); ret.min = range.min; ret.max = range.max; } else if (this.GetStance().respondChase) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var range = cmpVision.GetRange(); ret.max = range; } else if (this.GetStance().respondHoldGround) { var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return ret; var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack()); var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var halfvision = cmpVision.GetRange() / 2; ret.max = range.max + halfvision; } // We probably have stance 'passive' and we wouldn't have a range, // but as it is the default for healers we need to set it to something sane. else if (iid === IID_Heal) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var range = cmpVision.GetRange(); ret.max = range; } return ret; }; UnitAI.prototype.GetStance = function() { return g_Stances[this.stance]; }; UnitAI.prototype.GetStanceName = function() { return this.stance; }; UnitAI.prototype.SetMoveSpeed = function(speed) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.SetSpeed(speed); }; UnitAI.prototype.SetHeldPosition = function(x, z) { this.heldPosition = {"x": x, "z": z}; }; UnitAI.prototype.GetHeldPosition = function(pos) { return this.heldPosition; }; UnitAI.prototype.WalkToHeldPosition = function() { if (this.heldPosition) { this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false); return true; } return false; }; //// Helper functions //// UnitAI.prototype.CanAttack = function(target, forceResponse) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Attack commands var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; if (!cmpAttack.CanAttack(target)) return false; // Verify that the target is alive if (!this.TargetIsAlive(target)) return false; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; // Verify that the target is an attackable resource supply like a domestic animal // or that it isn't owned by an ally of this entity's player or is responding to // an attack. var owner = cmpOwnership.GetOwner(); if (!this.MustKillGatherTarget(target) && !(IsOwnedByEnemyOfPlayer(owner, target) || IsOwnedByNeutralOfPlayer(owner, target) || (forceResponse && !IsOwnedByPlayer(owner, target)))) return false; return true; }; UnitAI.prototype.CanGarrison = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; // Verify that the target is owned by this entity's player or a mutual ally of this player var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target))) return false; // Don't let animals garrison for now // (If we want to support that, we'll need to change Order.Garrison so it // doesn't move the animal into an INVIDIDUAL.* state) if (this.IsAnimal()) return false; return true; }; UnitAI.prototype.CanGather = function(target) { // The target must be a valid resource supply. var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); if (!cmpResourceSupply) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Gather commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; // Verify that we can gather from this target if (!cmpResourceGatherer.GetTargetGatherRate(target)) return false; // No need to verify ownership as we should be able to gather from // a target regardless of ownership. // No need to call "cmpResourceSupply.IsAvailable()" either because that // would cause units to walk to full entities instead of choosing another one // nearby to gather from, which is undesirable. return true; }; UnitAI.prototype.CanHeal = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Heal commands var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); if (!cmpHeal) return false; // Verify that the target is alive if (!this.TargetIsAlive(target)) return false; // Verify that the target is owned by the same player as the entity or of an ally var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))) return false; // Verify that the target is not unhealable (or at max health) var cmpHealth = Engine.QueryInterface(target, IID_Health); if (!cmpHealth || cmpHealth.IsUnhealable()) return false; // Verify that the target has no unhealable class var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return false; for each (var unhealableClass in cmpHeal.GetUnhealableClasses()) { if (cmpIdentity.HasClass(unhealableClass) != -1) { return false; } } // Verify that the target is a healable class var healable = false; for each (var healableClass in cmpHeal.GetHealableClasses()) { if (cmpIdentity.HasClass(healableClass) != -1) { healable = true; } } if (!healable) return false; return true; }; UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to ReturnResource commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; // Verify that the target is a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return false; if (checkCarriedResource) { // Verify that we are carrying some resources, // and can return our current resource to this target var type = cmpResourceGatherer.GetMainCarryingType(); if (!type || !cmpResourceDropsite.AcceptsType(type)) return false; } // Verify that the dropsite is owned by this entity's player var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target)) return false; return true; }; UnitAI.prototype.CanTrade = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Trade commands var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader || !cmpTrader.CanTrade(target)) return false; return true; }; UnitAI.prototype.CanRepair = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Repair (Builder) commands var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); if (!cmpBuilder) return false; // Verify that the target is owned by an ally of this entity's player var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)) return false; return true; }; UnitAI.prototype.CanPack = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && !cmpPack.IsPacking() && !cmpPack.IsPacked()); }; UnitAI.prototype.CanUnpack = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && !cmpPack.IsPacking() && cmpPack.IsPacked()); }; UnitAI.prototype.IsPacking = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && cmpPack.IsPacking()); }; //// Animal specific functions //// UnitAI.prototype.MoveRandomly = function(distance) { // We want to walk in a random direction, but avoid getting stuck // in obstacles or narrow spaces. // So pick a circular range from approximately our current position, // and move outwards to the nearest point on that circle, which will // lead to us avoiding obstacles and moving towards free space. // TODO: we probably ought to have a 'home' point, and drift towards // that, so we don't spread out all across the whole map var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition) return; if (!cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); var jitter = 0.5; // Randomly adjust the range's center a bit, so we tend to prefer // moving in random directions (if there's nothing in the way) var tx = pos.x + (2*Math.random()-1)*jitter; var tz = pos.z + (2*Math.random()-1)*jitter; var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.MoveToPointRange(tx, tz, distance, distance); }; UnitAI.prototype.AttackEntitiesByPreference = function(ents) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; return this.RespondToTargetedEntities( ents.filter(function (v, i, a) { return cmpAttack.CanAttack(v); }) .sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); }) ); }; UnitAI.prototype.AttackGaiaEntitiesByPreference = function(ents) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; const filter = function(e) { var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return (cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal())); }; return this.RespondToTargetedEntities( ents.filter(function (v, i, a) { return cmpAttack.CanAttack(v) && filter(v); }) .sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); }) ); }; Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml (revision 13625) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml (revision 13626) @@ -1,92 +1,93 @@ - - - - 0.0 - 20.0 - 0.0 - 70.0 - 16.0 - 75.0 - 1200 - 2000 - 1.5 - - + + + + 0.0 + 20.0 + 0.0 + 57.0 + 16.0 + 15 + 75.0 + 1200 + 2000 + 1.5 + + 1 1 DefenseTower 120 100 100 15.0 5 0.1 Support Infantry 0 2 1200 rubble/rubble_stone_2x2 Defense Tower Shoots arrows. Garrison to provide extra defense. Town Tower GarrisonTower -ConquestCritical structures/defense_tower.png phase_town 100 0 10 10 0 0.7 pair_tower_01 interface/complete/building/complete_tower.xml attack/weapon/arrowfly.xml attack/destruction/building_collapse_large.xml 6.0 0.6 21.0 false 32 65536 80 structures/fndn_2x2.xml Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h (revision 13625) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 13626) @@ -1,500 +1,500 @@ /* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_SCRIPTINTERFACE #define INCLUDED_SCRIPTINTERFACE #include #include #include #include "ScriptTypes.h" #include "ScriptVal.h" #include "js/jsapi.h" #include "lib/file/vfs/vfs_path.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include class AutoGCRooter; // Set the maximum number of function arguments that can be handled // (This should be as small as possible (for compiler efficiency), // but as large as necessary for all wrapped functions) -#define SCRIPT_INTERFACE_MAX_ARGS 7 +#define SCRIPT_INTERFACE_MAX_ARGS 8 // TODO: what's a good default? #define DEFAULT_RUNTIME_SIZE 16 * 1024 * 1024 struct ScriptInterface_impl; class ScriptRuntime; class CDebuggingServer; /** * Abstraction around a SpiderMonkey JSContext. * * Thread-safety: * - May be used in non-main threads. * - Each ScriptInterface must be created, used, and destroyed, all in a single thread * (it must never be shared between threads). */ class ScriptInterface { public: /** * Returns a runtime, which can used to initialise any number of * ScriptInterfaces contexts. Values created in one context may be used * in any other context from the same runtime (but not any other runtime). * Each runtime should only ever be used on a single thread. * @param runtimeSize Maximum size in bytes of the new runtime */ static shared_ptr CreateRuntime(int runtimeSize = DEFAULT_RUNTIME_SIZE); /** * Constructor. * @param nativeScopeName Name of global object that functions (via RegisterFunction) will * be placed into, as a scoping mechanism; typically "Engine" * @param debugName Name of this interface for CScriptStats purposes. * @param runtime ScriptRuntime to use when initializing this interface. */ ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& runtime); ~ScriptInterface(); /** * Shut down the JS system to clean up memory. Must only be called when there * are no ScriptInterfaces alive. */ static void ShutDown(); void SetCallbackData(void* cbdata); static void* GetCallbackData(JSContext* cx); JSContext* GetContext() const; JSRuntime* GetRuntime() const; /** * Load global scripts that most script contexts need, * located in the /globalscripts directory. VFS must be initialized. */ bool LoadGlobalScripts(); /** * Replace the default JS random number geenrator with a seeded, network-sync'd one. */ bool ReplaceNondeterministicRNG(boost::rand48& rng); /** * Call a constructor function, equivalent to JS "new ctor(arg)". * @return The new object; or JSVAL_VOID on failure, and logs an error message */ jsval CallConstructor(jsval ctor, jsval arg); /** * Create an object as with CallConstructor except don't actually execute the * constructor function. * @return The new object; or JSVAL_VOID on failure, and logs an error message */ jsval NewObjectFromConstructor(jsval ctor); /** * Call the named property on the given object, with void return type and 0 arguments */ bool CallFunctionVoid(jsval val, const char* name); /** * Call the named property on the given object, with void return type and 1 argument */ template bool CallFunctionVoid(jsval val, const char* name, const T0& a0); /** * Call the named property on the given object, with void return type and 2 arguments */ template bool CallFunctionVoid(jsval val, const char* name, const T0& a0, const T1& a1); /** * Call the named property on the given object, with void return type and 3 arguments */ template bool CallFunctionVoid(jsval val, const char* name, const T0& a0, const T1& a1, const T2& a2); /** * Call the named property on the given object, with return type R and 0 arguments */ template bool CallFunction(jsval val, const char* name, R& ret); /** * Call the named property on the given object, with return type R and 1 argument */ template bool CallFunction(jsval val, const char* name, const T0& a0, R& ret); /** * Call the named property on the given object, with return type R and 2 arguments */ template bool CallFunction(jsval val, const char* name, const T0& a0, const T1& a1, R& ret); /** * Call the named property on the given object, with return type R and 3 arguments */ template bool CallFunction(jsval val, const char* name, const T0& a0, const T1& a1, const T2& a2, R& ret); /** * Call the named property on the given object, with return type R and 4 arguments */ template bool CallFunction(jsval val, const char* name, const T0& a0, const T1& a1, const T2& a2, const T3& a3, R& ret); jsval GetGlobalObject(); JSClass* GetGlobalClass(); /** * Set the named property on the global object. * If @p replace is true, an existing property will be overwritten; otherwise attempts * to set an already-defined value will fail. */ template bool SetGlobal(const char* name, const T& value, bool replace = false); /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(jsval obj, const char* name, const T& value, bool constant = false, bool enumerate = true); /** * Set the integer-named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetPropertyInt(jsval obj, int name, const T& value, bool constant = false, bool enumerate = true); /** * Get the named property on the given object. */ template bool GetProperty(jsval obj, const char* name, T& out); /** * Get the integer-named property on the given object. */ template bool GetPropertyInt(jsval obj, int name, T& out); /** * Check the named property has been defined on the given object. */ bool HasProperty(jsval obj, const char* name); bool EnumeratePropertyNamesWithPrefix(jsval obj, const char* prefix, std::vector& out); bool SetPrototype(jsval obj, jsval proto); bool FreezeObject(jsval obj, bool deep); bool Eval(const char* code); template bool Eval(const CHAR* code, T& out); std::wstring ToString(jsval obj, bool pretty = false); /** * Parse a UTF-8-encoded JSON string. Returns the undefined value on error. */ CScriptValRooted ParseJSON(const std::string& string_utf8); /** * Read a JSON file. Returns the undefined value on error. */ CScriptValRooted ReadJSONFile(const VfsPath& path); /** * Stringify to a JSON string, UTF-8 encoded. Returns an empty string on error. */ std::string StringifyJSON(jsval obj, bool indent = true); /** * Report the given error message through the JS error reporting mechanism, * and throw a JS exception. (Callers can check IsPendingException, and must * return JS_FALSE in that case to propagate the exception.) */ void ReportError(const char* msg); /** * Load and execute the given script in a new function scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadScript(const VfsPath& filename, const std::string& code); /** * Load and execute the given script in the global scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScript(const VfsPath& filename, const std::string& code); /** * Load and execute the given script in the global scope. * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScriptFile(const VfsPath& path); /** * Construct a new value (usable in this ScriptInterface's context) by cloning * a value from a different context. * Complex values (functions, XML, etc) won't be cloned correctly, but basic * types and cyclic references should be fine. */ jsval CloneValueFromOtherContext(ScriptInterface& otherContext, jsval val); /** * Convert a jsval to a C++ type. (This might trigger GC.) */ template static bool FromJSVal(JSContext* cx, jsval val, T& ret); /** * Convert a C++ type to a jsval. (This might trigger GC. The return * value must be rooted if you don't want it to be collected.) */ template static jsval ToJSVal(JSContext* cx, T const& val); AutoGCRooter* ReplaceAutoGCRooter(AutoGCRooter* rooter); /** * Dump some memory heap debugging information to stderr. */ void DumpHeap(); /** * MaybeGC tries to determine whether garbage collection in cx's runtime would free up enough memory to be worth the amount of time it would take */ void MaybeGC(); /** * Structured clones are a way to serialize 'simple' JS values into a buffer * that can safely be passed between contexts and runtimes and threads. * A StructuredClone can be stored and read multiple times if desired. * We wrap them in shared_ptr so memory management is automatic and * thread-safe. */ class StructuredClone { NONCOPYABLE(StructuredClone); public: StructuredClone(); ~StructuredClone(); JSContext* m_Context; uint64* m_Data; size_t m_Size; }; shared_ptr WriteStructuredClone(jsval v); jsval ReadStructuredClone(const shared_ptr& ptr); private: bool CallFunction_(jsval val, const char* name, size_t argc, jsval* argv, jsval& ret); bool Eval_(const char* code, jsval& ret); bool Eval_(const wchar_t* code, jsval& ret); bool SetGlobal_(const char* name, jsval value, bool replace); bool SetProperty_(jsval obj, const char* name, jsval value, bool readonly, bool enumerate); bool SetPropertyInt_(jsval obj, int name, jsval value, bool readonly, bool enumerate); bool GetProperty_(jsval obj, const char* name, jsval& value); bool GetPropertyInt_(jsval obj, int name, jsval& value); static bool IsExceptionPending(JSContext* cx); static JSClass* GetClass(JSContext* cx, JSObject* obj); static void* GetPrivate(JSContext* cx, JSObject* obj); void Register(const char* name, JSNative fptr, size_t nargs); std::auto_ptr m; // The nasty macro/template bits are split into a separate file so you don't have to look at them public: #include "NativeWrapperDecls.h" // This declares: // // template // void RegisterFunction(const char* functionName); // // template // static JSNative call; // // template // static JSNative callMethod; // // template // static size_t nargs(); }; // Implement those declared functions #include "NativeWrapperDefns.h" template bool ScriptInterface::CallFunction(jsval val, const char* name, R& ret) { jsval jsRet; bool ok = CallFunction_(val, name, 0, NULL, jsRet); if (!ok) return false; return FromJSVal(GetContext(), jsRet, ret); } template bool ScriptInterface::CallFunctionVoid(jsval val, const char* name, const T0& a0) { jsval jsRet; jsval argv[1]; argv[0] = ToJSVal(GetContext(), a0); return CallFunction_(val, name, 1, argv, jsRet); } template bool ScriptInterface::CallFunctionVoid(jsval val, const char* name, const T0& a0, const T1& a1) { jsval jsRet; jsval argv[2]; argv[0] = ToJSVal(GetContext(), a0); argv[1] = ToJSVal(GetContext(), a1); return CallFunction_(val, name, 2, argv, jsRet); } template bool ScriptInterface::CallFunctionVoid(jsval val, const char* name, const T0& a0, const T1& a1, const T2& a2) { jsval jsRet; jsval argv[3]; argv[0] = ToJSVal(GetContext(), a0); argv[1] = ToJSVal(GetContext(), a1); argv[2] = ToJSVal(GetContext(), a2); return CallFunction_(val, name, 3, argv, jsRet); } template bool ScriptInterface::CallFunction(jsval val, const char* name, const T0& a0, R& ret) { jsval jsRet; jsval argv[1]; argv[0] = ToJSVal(GetContext(), a0); bool ok = CallFunction_(val, name, 1, argv, jsRet); if (!ok) return false; return FromJSVal(GetContext(), jsRet, ret); } template bool ScriptInterface::CallFunction(jsval val, const char* name, const T0& a0, const T1& a1, R& ret) { jsval jsRet; jsval argv[2]; argv[0] = ToJSVal(GetContext(), a0); argv[1] = ToJSVal(GetContext(), a1); bool ok = CallFunction_(val, name, 2, argv, jsRet); if (!ok) return false; return FromJSVal(GetContext(), jsRet, ret); } template bool ScriptInterface::CallFunction(jsval val, const char* name, const T0& a0, const T1& a1, const T2& a2, R& ret) { jsval jsRet; jsval argv[3]; argv[0] = ToJSVal(GetContext(), a0); argv[1] = ToJSVal(GetContext(), a1); argv[2] = ToJSVal(GetContext(), a2); bool ok = CallFunction_(val, name, 3, argv, jsRet); if (!ok) return false; return FromJSVal(GetContext(), jsRet, ret); } template bool ScriptInterface::CallFunction(jsval val, const char* name, const T0& a0, const T1& a1, const T2& a2, const T3& a3, R& ret) { jsval jsRet; jsval argv[4]; argv[0] = ToJSVal(GetContext(), a0); argv[1] = ToJSVal(GetContext(), a1); argv[2] = ToJSVal(GetContext(), a2); argv[3] = ToJSVal(GetContext(), a3); bool ok = CallFunction_(val, name, 4, argv, jsRet); if (!ok) return false; return FromJSVal(GetContext(), jsRet, ret); } template bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace) { return SetGlobal_(name, ToJSVal(GetContext(), value), replace); } template bool ScriptInterface::SetProperty(jsval obj, const char* name, const T& value, bool readonly, bool enumerate) { return SetProperty_(obj, name, ToJSVal(GetContext(), value), readonly, enumerate); } template bool ScriptInterface::SetPropertyInt(jsval obj, int name, const T& value, bool readonly, bool enumerate) { return SetPropertyInt_(obj, name, ToJSVal(GetContext(), value), readonly, enumerate); } template bool ScriptInterface::GetProperty(jsval obj, const char* name, T& out) { jsval val; if (! GetProperty_(obj, name, val)) return false; return FromJSVal(GetContext(), val, out); } template bool ScriptInterface::GetPropertyInt(jsval obj, int name, T& out) { jsval val; if (! GetPropertyInt_(obj, name, val)) return false; return FromJSVal(GetContext(), val, out); } template bool ScriptInterface::Eval(const CHAR* code, T& ret) { jsval rval; if (! Eval_(code, rval)) return false; return FromJSVal(GetContext(), rval, ret); } #endif // INCLUDED_SCRIPTINTERFACE Index: ps/trunk/source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 13625) +++ ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 13626) @@ -1,1478 +1,1750 @@ /* Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpRangeManager.h" +#include "ICmpTerrain.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/components/ICmpVision.h" +#include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/Render.h" #include "simulation2/helpers/Spatial.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "maths/FixedVector2D.h" #include "ps/CLogger.h" #include "ps/Overlay.h" #include "ps/Profile.h" #include "renderer/Scene.h" #define DEBUG_RANGE_MANAGER_BOUNDS 0 /** * Representation of a range query. */ struct Query { bool enabled; + bool parabolic; entity_id_t source; entity_pos_t minRange; entity_pos_t maxRange; + entity_pos_t elevationBonus; u32 ownersMask; i32 interface; std::vector lastMatch; u8 flagsMask; }; /** * Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players) * into a 32-bit mask for quick set-membership tests. */ static u32 CalcOwnerMask(player_id_t owner) { if (owner >= -1 && owner < 31) return 1 << (1+owner); else return 0; // owner was invalid } /** * Returns LOS mask for given player. */ static u32 CalcPlayerLosMask(player_id_t player) { if (player > 0 && player <= 16) return ICmpRangeManager::LOS_MASK << (2*(player-1)); return 0; } /** * Returns shared LOS mask for given list of players. */ static u32 CalcSharedLosMask(std::vector players) { u32 playerMask = 0; for (size_t i = 0; i < players.size(); i++) playerMask |= CalcPlayerLosMask(players[i]); return playerMask; } /** + * Checks whether v is in a parabolic range of (0,0,0) + * The highest point of the paraboloid is (0,range/2,0) + * and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid + * + * Avoids sqrting and overflowing. + */ +static bool InParabolicRange(CFixedVector3D v, fixed range) +{ + i64 x = (i64)v.X.GetInternalValue(); // abs(x) <= 2^31 + i64 z = (i64)v.Z.GetInternalValue(); + i64 xx = (x * x); // xx <= 2^62 + i64 zz = (z * z); + i64 d2 = (xx + zz) >> 1; // d2 <= 2^62 (no overflow) + + i64 y = (i64)v.Y.GetInternalValue(); + + i64 c = (i64)range.GetInternalValue(); + i64 c_2 = c >> 1; + + i64 c2 = (c_2-y)*c; + + if (d2 <= c2) + return true; + + return false; +} + +struct EntityParabolicRangeOutline +{ + entity_id_t source; + CFixedVector3D position; + entity_pos_t range; + std::vector outline; +}; + +static std::map ParabolicRangesOutlines; + +/** * Representation of an entity, with the data needed for queries. */ struct EntityData { EntityData() : retainInFog(0), owner(-1), inWorld(0), flags(1) { } entity_pos_t x, z; entity_pos_t visionRange; u8 retainInFog; // boolean i8 owner; u8 inWorld; // boolean u8 flags; // See GetEntityFlagMask }; cassert(sizeof(EntityData) == 16); /** * Serialization helper template for Query */ struct SerializeQuery { template void operator()(S& serialize, const char* UNUSED(name), Query& value) { serialize.Bool("enabled", value.enabled); + serialize.Bool("parabolic",value.parabolic); serialize.NumberU32_Unbounded("source", value.source); serialize.NumberFixed_Unbounded("min range", value.minRange); serialize.NumberFixed_Unbounded("max range", value.maxRange); + serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus); serialize.NumberU32_Unbounded("owners mask", value.ownersMask); serialize.NumberI32_Unbounded("interface", value.interface); SerializeVector()(serialize, "last match", value.lastMatch); serialize.NumberU8_Unbounded("flagsMask", value.flagsMask); } }; /** * Serialization helper template for EntityData */ struct SerializeEntityData { template void operator()(S& serialize, const char* UNUSED(name), EntityData& value) { serialize.NumberFixed_Unbounded("x", value.x); serialize.NumberFixed_Unbounded("z", value.z); serialize.NumberFixed_Unbounded("vision", value.visionRange); serialize.NumberU8("retain in fog", value.retainInFog, 0, 1); serialize.NumberI8_Unbounded("owner", value.owner); serialize.NumberU8("in world", value.inWorld, 0, 1); serialize.NumberU8_Unbounded("flags", value.flags); } }; /** * Functor for sorting entities by distance from a source point. * It must only be passed entities that are in 'entities' * and are currently in the world. */ struct EntityDistanceOrdering { EntityDistanceOrdering(const std::map& entities, const CFixedVector2D& source) : m_EntityData(entities), m_Source(source) { } bool operator()(entity_id_t a, entity_id_t b) { const EntityData& da = m_EntityData.find(a)->second; const EntityData& db = m_EntityData.find(b)->second; CFixedVector2D vecA = CFixedVector2D(da.x, da.z) - m_Source; CFixedVector2D vecB = CFixedVector2D(db.x, db.z) - m_Source; return (vecA.CompareLength(vecB) < 0); } const std::map& m_EntityData; CFixedVector2D m_Source; private: EntityDistanceOrdering& operator=(const EntityDistanceOrdering&); }; /** * Range manager implementation. * Maintains a list of all entities (and their positions and owners), which is used for * queries. * * LOS implementation is based on the model described in GPG2. * (TODO: would be nice to make it cleverer, so e.g. mountains and walls * can block vision) */ class CCmpRangeManager : public ICmpRangeManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Create); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_Destroy); componentManager.SubscribeGloballyToMessageType(MT_VisionRangeChanged); componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays } DEFAULT_COMPONENT_ALLOCATOR(RangeManager) bool m_DebugOverlayEnabled; bool m_DebugOverlayDirty; std::vector m_DebugOverlayLines; // World bounds (entities are expected to be within this range) entity_pos_t m_WorldX0; entity_pos_t m_WorldZ0; entity_pos_t m_WorldX1; entity_pos_t m_WorldZ1; // Range query state: tag_t m_QueryNext; // next allocated id std::map m_Queries; std::map m_EntityData; SpatialSubdivision m_Subdivision; // spatial index of m_EntityData // LOS state: std::map m_LosRevealAll; bool m_LosCircular; i32 m_TerrainVerticesPerSide; size_t m_TerritoriesDirtyID; // Counts of units seeing vertex, per vertex, per player (starting with player 0). // Use u16 to avoid overflows when we have very large (but not infeasibly large) numbers // of units in a very small area. // (Note we use vertexes, not tiles, to better match the renderer.) // Lazily constructed when it's needed, to save memory in smaller games. std::vector > m_LosPlayerCounts; // 2-bit ELosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive) std::vector m_LosState; static const player_id_t MAX_LOS_PLAYER_ID = 16; // Special static visibility data for the "reveal whole map" mode // (TODO: this is usually a waste of memory) std::vector m_LosStateRevealed; // Shared LOS masks, one per player. std::map m_SharedLosMasks; // Cache explored vertices per player (not serialized) u32 m_TotalInworldVertices; std::vector m_ExploredVertices; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_QueryNext = 1; m_DebugOverlayEnabled = false; m_DebugOverlayDirty = true; m_WorldX0 = m_WorldZ0 = m_WorldX1 = m_WorldZ1 = entity_pos_t::Zero(); // Initialise with bogus values (these will get replaced when // SetBounds is called) ResetSubdivisions(entity_pos_t::FromInt(1), entity_pos_t::FromInt(1)); // The whole map should be visible to Gaia by default, else e.g. animals // will get confused when trying to run from enemies m_LosRevealAll[0] = true; // This is not really an error condition, an entity recently created or destroyed // might have an owner of INVALID_PLAYER m_SharedLosMasks[INVALID_PLAYER] = 0; m_LosCircular = false; m_TerrainVerticesPerSide = 0; m_TerritoriesDirtyID = 0; } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.NumberFixed_Unbounded("world x0", m_WorldX0); serialize.NumberFixed_Unbounded("world z0", m_WorldZ0); serialize.NumberFixed_Unbounded("world x1", m_WorldX1); serialize.NumberFixed_Unbounded("world z1", m_WorldZ1); serialize.NumberU32_Unbounded("query next", m_QueryNext); SerializeMap()(serialize, "queries", m_Queries); SerializeMap()(serialize, "entity data", m_EntityData); SerializeMap()(serialize, "los reveal all", m_LosRevealAll); serialize.Bool("los circular", m_LosCircular); serialize.NumberI32_Unbounded("terrain verts per side", m_TerrainVerticesPerSide); // We don't serialize m_Subdivision or m_LosPlayerCounts // since they can be recomputed from the entity data when deserializing; // m_LosState must be serialized since it depends on the history of exploration SerializeVector()(serialize, "los state", m_LosState); SerializeMap()(serialize, "shared los masks", m_SharedLosMasks); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); // Reinitialise subdivisions and LOS data ResetDerivedData(true); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Create: { const CMessageCreate& msgData = static_cast (msg); entity_id_t ent = msgData.entity; // Ignore local entities - we shouldn't let them influence anything if (ENTITY_IS_LOCAL(ent)) break; // Ignore non-positional entities CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition) break; // The newly-created entity will have owner -1 and position out-of-world // (any initialisation of those values will happen later), so we can just // use the default-constructed EntityData here EntityData entdata; // Store the LOS data, if any CmpPtr cmpVision(GetSimContext(), ent); if (cmpVision) { entdata.visionRange = cmpVision->GetRange(); entdata.retainInFog = (cmpVision->GetRetainInFog() ? 1 : 0); } // Remember this entity m_EntityData.insert(std::make_pair(ent, entdata)); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; std::map::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (msgData.inWorld) { if (it->second.inWorld) { CFixedVector2D from(it->second.x, it->second.z); CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Move(ent, from, to); LosMove(it->second.owner, it->second.visionRange, from, to); } else { CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Add(ent, to); LosAdd(it->second.owner, it->second.visionRange, to); } it->second.inWorld = 1; it->second.x = msgData.x; it->second.z = msgData.z; } else { if (it->second.inWorld) { CFixedVector2D from(it->second.x, it->second.z); m_Subdivision.Remove(ent, from); LosRemove(it->second.owner, it->second.visionRange, from); } it->second.inWorld = 0; it->second.x = entity_pos_t::Zero(); it->second.z = entity_pos_t::Zero(); } break; } case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; std::map::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.inWorld) { CFixedVector2D pos(it->second.x, it->second.z); LosRemove(it->second.owner, it->second.visionRange, pos); LosAdd(msgData.to, it->second.visionRange, pos); } ENSURE(-128 <= msgData.to && msgData.to <= 127); it->second.owner = (i8)msgData.to; break; } case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); entity_id_t ent = msgData.entity; std::map::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.inWorld) m_Subdivision.Remove(ent, CFixedVector2D(it->second.x, it->second.z)); // This will be called after Ownership's OnDestroy, so ownership will be set // to -1 already and we don't have to do a LosRemove here ENSURE(it->second.owner == -1); m_EntityData.erase(it); break; } case MT_VisionRangeChanged: { const CMessageVisionRangeChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; std::map::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; CmpPtr cmpVision(GetSimContext(), ent); if (!cmpVision) break; entity_pos_t oldRange = it->second.visionRange; entity_pos_t newRange = msgData.newRange; // If the range changed and the entity's in-world, we need to manually adjust it // but if it's not in-world, we only need to set the new vision range CFixedVector2D pos(it->second.x, it->second.z); if (it->second.inWorld) LosRemove(it->second.owner, oldRange, pos); it->second.visionRange = newRange; if (it->second.inWorld) LosAdd(it->second.owner, newRange, pos); break; } case MT_Update: { m_DebugOverlayDirty = true; UpdateTerritoriesLos(); ExecuteActiveQueries(); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } } } virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) { m_WorldX0 = x0; m_WorldZ0 = z0; m_WorldX1 = x1; m_WorldZ1 = z1; m_TerrainVerticesPerSide = (i32)vertices; ResetDerivedData(false); } virtual void Verify() { // Ignore if map not initialised yet if (m_WorldX1.IsZero()) return; // Check that calling ResetDerivedData (i.e. recomputing all the state from scratch) // does not affect the incrementally-computed state std::vector > oldPlayerCounts = m_LosPlayerCounts; std::vector oldStateRevealed = m_LosStateRevealed; SpatialSubdivision oldSubdivision = m_Subdivision; ResetDerivedData(true); if (oldPlayerCounts != m_LosPlayerCounts) { for (size_t i = 0; i < oldPlayerCounts.size(); ++i) { debug_printf(L"%d: ", (int)i); for (size_t j = 0; j < oldPlayerCounts[i].size(); ++j) debug_printf(L"%d ", oldPlayerCounts[i][j]); debug_printf(L"\n"); } for (size_t i = 0; i < m_LosPlayerCounts.size(); ++i) { debug_printf(L"%d: ", (int)i); for (size_t j = 0; j < m_LosPlayerCounts[i].size(); ++j) debug_printf(L"%d ", m_LosPlayerCounts[i][j]); debug_printf(L"\n"); } debug_warn(L"inconsistent player counts"); } if (oldStateRevealed != m_LosStateRevealed) debug_warn(L"inconsistent revealed"); if (oldSubdivision != m_Subdivision) debug_warn(L"inconsistent subdivs"); } // Reinitialise subdivisions and LOS data, based on entity data void ResetDerivedData(bool skipLosState) { ENSURE(m_WorldX0.IsZero() && m_WorldZ0.IsZero()); // don't bother implementing non-zero offsets yet ResetSubdivisions(m_WorldX1, m_WorldZ1); m_LosPlayerCounts.clear(); m_LosPlayerCounts.resize(MAX_LOS_PLAYER_ID+1); m_ExploredVertices.clear(); m_ExploredVertices.resize(MAX_LOS_PLAYER_ID+1, 0); if (skipLosState) { // recalc current exploration stats. for (i32 j = 0; j < m_TerrainVerticesPerSide; j++) { for (i32 i = 0; i < m_TerrainVerticesPerSide; i++) { if (!LosIsOffWorld(i, j)) { for (u8 k = 1; k < MAX_LOS_PLAYER_ID+1; ++k) m_ExploredVertices.at(k) += ((m_LosState[j*m_TerrainVerticesPerSide + i] & (LOS_EXPLORED << (2*(k-1)))) > 0); } } } } else { m_LosState.clear(); m_LosState.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); } m_LosStateRevealed.clear(); m_LosStateRevealed.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); for (std::map::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { if (it->second.inWorld) LosAdd(it->second.owner, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); } m_TotalInworldVertices = 0; for (ssize_t j = 0; j < m_TerrainVerticesPerSide; ++j) for (ssize_t i = 0; i < m_TerrainVerticesPerSide; ++i) { if (LosIsOffWorld(i,j)) m_LosStateRevealed[i + j*m_TerrainVerticesPerSide] = 0; else { m_LosStateRevealed[i + j*m_TerrainVerticesPerSide] = 0xFFFFFFFFu; m_TotalInworldVertices++; } } } void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1) { // Use 8x8 tile subdivisions // (TODO: find the optimal number instead of blindly guessing) m_Subdivision.Reset(x1, z1, entity_pos_t::FromInt(8*TERRAIN_TILE_SIZE)); for (std::map::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { if (it->second.inWorld) m_Subdivision.Add(it->first, CFixedVector2D(it->second.x, it->second.z)); } } virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, std::vector owners, int requiredInterface, u8 flags) { tag_t id = m_QueryNext++; m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags); return id; } + virtual tag_t CreateActiveParabolicQuery(entity_id_t source, + entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, + std::vector owners, int requiredInterface, u8 flags) + { + tag_t id = m_QueryNext++; + m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, elevationBonus, owners, requiredInterface, flags); + + return id; + } + virtual void DestroyActiveQuery(tag_t tag) { if (m_Queries.find(tag) == m_Queries.end()) { LOGERROR(L"CCmpRangeManager: DestroyActiveQuery called with invalid tag %u", tag); return; } m_Queries.erase(tag); } virtual void EnableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR(L"CCmpRangeManager: EnableActiveQuery called with invalid tag %u", tag); return; } Query& q = it->second; q.enabled = true; } virtual void DisableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR(L"CCmpRangeManager: DisableActiveQuery called with invalid tag %u", tag); return; } Query& q = it->second; q.enabled = false; } virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, std::vector owners, int requiredInterface) { PROFILE("ExecuteQuery"); Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal")); std::vector r; CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list return r; } PerformQuery(q, r); // Return the list sorted by distance from the entity CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector ResetActiveQuery(tag_t tag) { PROFILE("ResetActiveQuery"); std::vector r; std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR(L"CCmpRangeManager: ResetActiveQuery called with invalid tag %u", tag); return r; } Query& q = it->second; q.enabled = true; CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list q.lastMatch = r; return r; } PerformQuery(q, r); q.lastMatch = r; // Return the list sorted by distance from the entity CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector GetEntitiesByPlayer(player_id_t player) { std::vector entities; u32 ownerMask = CalcOwnerMask(player); for (std::map::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { // Check owner and add to list if it matches if (CalcOwnerMask(it->second.owner) & ownerMask) entities.push_back(it->first); } return entities; } virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; m_DebugOverlayDirty = true; if (!enabled) m_DebugOverlayLines.clear(); } /** * Update all currently-enabled active queries. */ void ExecuteActiveQueries() { PROFILE3("ExecuteActiveQueries"); // Store a queue of all messages before sending any, so we can assume // no entities will move until we've finished checking all the ranges std::vector > messages; for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& q = it->second; if (!q.enabled) continue; CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) continue; std::vector r; r.reserve(q.lastMatch.size()); PerformQuery(q, r); // Compute the changes vs the last match std::vector added; std::vector removed; std::set_difference(r.begin(), r.end(), q.lastMatch.begin(), q.lastMatch.end(), std::back_inserter(added)); std::set_difference(q.lastMatch.begin(), q.lastMatch.end(), r.begin(), r.end(), std::back_inserter(removed)); if (added.empty() && removed.empty()) continue; // Return the 'added' list sorted by distance from the entity // (Don't bother sorting 'removed' because they might not even have positions or exist any more) CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, pos)); messages.push_back(std::make_pair(q.source, CMessageRangeUpdate(it->first))); messages.back().second.added.swap(added); messages.back().second.removed.swap(removed); it->second.lastMatch.swap(r); } for (size_t i = 0; i < messages.size(); ++i) GetSimContext().GetComponentManager().PostMessage(messages[i].first, messages[i].second); } /** * Returns whether the given entity matches the given query (ignoring maxRange) */ bool TestEntityQuery(const Query& q, entity_id_t id, const EntityData& entity) { // Quick filter to ignore entities with the wrong owner if (!(CalcOwnerMask(entity.owner) & q.ownersMask)) return false; // Ignore entities not present in the world if (!entity.inWorld) return false; // Ignore entities that don't match the current flags if (!(entity.flags & q.flagsMask)) return false; // Ignore self if (id == q.source) return false; // Ignore if it's missing the required interface if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface)) return false; return true; } /** * Returns a list of distinct entity IDs that match the given query, sorted by ID. */ void PerformQuery(const Query& q, std::vector& r) { CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) return; CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); // Special case: range -1.0 means check all entities ignoring distance if (q.maxRange == entity_pos_t::FromInt(-1)) { for (std::map::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { if (!TestEntityQuery(q, it->first, it->second)) continue; r.push_back(it->first); } } - else + // Not the entire world, so check a parabolic range, or a regular range + else if (q.parabolic) + { + // elevationBonus is part of the 3D position, as the source is really that much heigher + CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+ + CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ; + // Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange + std::vector ents = m_Subdivision.GetNear(pos, q.maxRange*2); + + for (size_t i = 0; i < ents.size(); ++i) + { + std::map::const_iterator it = m_EntityData.find(ents[i]); + ENSURE(it != m_EntityData.end()); + + if (!TestEntityQuery(q, it->first, it->second)) + continue; + + CmpPtr cmpSecondPosition(GetSimContext(), ents[i]); + if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld()) + continue; + CFixedVector3D secondPosition = cmpSecondPosition->GetPosition(); + + // Restrict based on precise distance + if (!InParabolicRange( + CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) + - pos3d, + q.maxRange)) + continue; + + if (!q.minRange.IsZero()) + { + int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); + if (distVsMin < 0) + continue; + } + + r.push_back(it->first); + + } + } + // check a regular range (i.e. not the entire world, and not parabolic) + else { // Get a quick list of entities that are potentially in range std::vector ents = m_Subdivision.GetNear(pos, q.maxRange); for (size_t i = 0; i < ents.size(); ++i) { std::map::const_iterator it = m_EntityData.find(ents[i]); ENSURE(it != m_EntityData.end()); if (!TestEntityQuery(q, it->first, it->second)) continue; // Restrict based on precise distance int distVsMax = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange); if (distVsMax > 0) continue; if (!q.minRange.IsZero()) { int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); if (distVsMin < 0) continue; } r.push_back(it->first); + } } } + + virtual entity_pos_t GetElevationAdaptedRange(CFixedVector3D pos, CFixedVector3D rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) + { + entity_pos_t r = entity_pos_t::Zero() ; + + pos.Y += elevationBonus; + entity_pos_t orientation = rot.Y; + + entity_pos_t maxAngle = orientation + angle/2; + entity_pos_t minAngle = orientation - angle/2; + + int numberOfSteps = 16; + + if (angle == entity_pos_t::Zero()) + numberOfSteps = 1; + + std::vector coords = getParabolicRangeForm(pos, range, range*2, minAngle, maxAngle, numberOfSteps); + + entity_pos_t part = entity_pos_t::FromInt(numberOfSteps); + + for (int i = 0; i < numberOfSteps; i++) + { + r = r + CFixedVector2D(coords[2*i],coords[2*i+1]).Length() / part; + } + + return r; + + } + + virtual std::vector getParabolicRangeForm(CFixedVector3D pos, entity_pos_t maxRange, entity_pos_t cutoff, entity_pos_t minAngle, entity_pos_t maxAngle, int numberOfSteps) + { + + // angle = 0 goes in the positive Z direction + entity_pos_t precision = entity_pos_t::FromInt((int)TERRAIN_TILE_SIZE)/8; + + std::vector r; + + + CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); + CmpPtr cmpWaterManager(GetSimContext(), SYSTEM_ENTITY); + entity_pos_t waterLevel = cmpWaterManager->GetWaterLevel(pos.X,pos.Z); + entity_pos_t thisHeight = pos.Y > waterLevel ? pos.Y : waterLevel; + + if (cmpTerrain) + { + for (int i = 0; i < numberOfSteps; i++) + { + entity_pos_t angle = minAngle + (maxAngle - minAngle) / numberOfSteps * i; + entity_pos_t sin; + entity_pos_t cos; + entity_pos_t minDistance = entity_pos_t::Zero(); + entity_pos_t maxDistance = cutoff; + sincos_approx(angle,sin,cos); + + CFixedVector2D minVector = CFixedVector2D(entity_pos_t::Zero(),entity_pos_t::Zero()); + CFixedVector2D maxVector = CFixedVector2D(sin,cos).Multiply(cutoff); + entity_pos_t targetHeight = cmpTerrain->GetGroundLevel(pos.X+maxVector.X,pos.Z+maxVector.Y); + // use water level to display range on water + targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; + + if (InParabolicRange(CFixedVector3D(maxVector.X,targetHeight-thisHeight,maxVector.Y),maxRange)) + { + r.push_back(maxVector.X); + r.push_back(maxVector.Y); + continue; + } + + // Loop until vectors come close enough + while ((maxVector - minVector).CompareLength(precision) > 0) + { + // difference still bigger than precision, bisect to get smaller difference + entity_pos_t newDistance = (minDistance+maxDistance)/entity_pos_t::FromInt(2); + + CFixedVector2D newVector = CFixedVector2D(sin,cos).Multiply(newDistance); + + // get the height of the ground + targetHeight = cmpTerrain->GetGroundLevel(pos.X+newVector.X,pos.Z+newVector.Y); + targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; + + if (InParabolicRange(CFixedVector3D(newVector.X,targetHeight-thisHeight,newVector.Y),maxRange)) + { + // new vector is in parabolic range, so this is a new minVector + minVector = newVector; + minDistance = newDistance; + } + else + { + // new vector is out parabolic range, so this is a new maxVector + maxVector = newVector; + maxDistance = newDistance; + } + + } + r.push_back(maxVector.X); + r.push_back(maxVector.Y); + + } + r.push_back(r[0]); + r.push_back(r[1]); + + } + return r; + + } + Query ConstructQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flagsMask) { // Min range must be non-negative if (minRange < entity_pos_t::Zero()) LOGWARNING(L"CCmpRangeManager: Invalid min range %f in query for entity %u", minRange.ToDouble(), source); // Max range must be non-negative, or else -1 if (maxRange < entity_pos_t::Zero() && maxRange != entity_pos_t::FromInt(-1)) LOGWARNING(L"CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source); Query q; q.enabled = false; + q.parabolic = false; q.source = source; q.minRange = minRange; q.maxRange = maxRange; + q.elevationBonus = entity_pos_t::Zero(); q.ownersMask = 0; for (size_t i = 0; i < owners.size(); ++i) q.ownersMask |= CalcOwnerMask(owners[i]); q.interface = requiredInterface; q.flagsMask = flagsMask; return q; } + Query ConstructParabolicQuery(entity_id_t source, + entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, + const std::vector& owners, int requiredInterface, u8 flagsMask) + { + Query q = ConstructQuery(source,minRange,maxRange,owners,requiredInterface,flagsMask); + q.parabolic = true; + q.elevationBonus = elevationBonus; + return q; + } + + void RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; - CColor enabledRingColour(0, 1, 0, 1); CColor disabledRingColour(1, 0, 0, 1); CColor rayColour(1, 1, 0, 0.2f); if (m_DebugOverlayDirty) { m_DebugOverlayLines.clear(); for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& q = it->second; CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) continue; CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); // Draw the max range circle - m_DebugOverlayLines.push_back(SOverlayLine()); - m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour); - SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); + if (!q.parabolic) + { + m_DebugOverlayLines.push_back(SOverlayLine()); + m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour); + SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); + } + else + { + // elevation bonus is part of the 3D position. As if the unit is really that much higher + CFixedVector3D pos = cmpSourcePosition->GetPosition(); + pos.Y += q.elevationBonus; + + std::vector coords; + + // Get the outline from cache if possible + if (ParabolicRangesOutlines.find(q.source) != ParabolicRangesOutlines.end()) + { + EntityParabolicRangeOutline e = ParabolicRangesOutlines[q.source]; + if (e.position == pos && e.range == q.maxRange) + { + // outline is cached correctly, use it + coords = e.outline; + } + else + { + // outline was cached, but important parameters changed + // (position, elevation, range) + // update it + coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); + e.outline = coords; + e.range = q.maxRange; + e.position = pos; + ParabolicRangesOutlines[q.source] = e; + } + } + else + { + // outline wasn't cached (first time you enable the range overlay + // or you created a new entiy) + // cache a new outline + coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); + EntityParabolicRangeOutline e; + e.source = q.source; + e.range = q.maxRange; + e.position = pos; + e.outline = coords; + ParabolicRangesOutlines[q.source] = e; + } + + CColor thiscolor = q.enabled ? enabledRingColour : disabledRingColour; + + // draw the outline (piece by piece) + for (size_t i = 3; i < coords.size(); i += 2) + { + std::vector c; + c.push_back((coords[i-3]+pos.X).ToFloat()); + c.push_back((coords[i-2]+pos.Z).ToFloat()); + c.push_back((coords[i-1]+pos.X).ToFloat()); + c.push_back((coords[i]+pos.Z).ToFloat()); + m_DebugOverlayLines.push_back(SOverlayLine()); + m_DebugOverlayLines.back().m_Color = thiscolor; + SimRender::ConstructLineOnGround(GetSimContext(), c, m_DebugOverlayLines.back(), true); + } + } // Draw the min range circle if (!q.minRange.IsZero()) { - m_DebugOverlayLines.push_back(SOverlayLine()); - m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour); SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.minRange.ToFloat(), m_DebugOverlayLines.back(), true); } // Draw a ray from the source to each matched entity for (size_t i = 0; i < q.lastMatch.size(); ++i) { CmpPtr cmpTargetPosition(GetSimContext(), q.lastMatch[i]); if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) continue; CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); std::vector coords; coords.push_back(pos.X.ToFloat()); coords.push_back(pos.Y.ToFloat()); coords.push_back(targetPos.X.ToFloat()); coords.push_back(targetPos.Y.ToFloat()); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = rayColour; SimRender::ConstructLineOnGround(GetSimContext(), coords, m_DebugOverlayLines.back(), true); } } m_DebugOverlayDirty = false; } for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) collector.Submit(&m_DebugOverlayLines[i]); } virtual u8 GetEntityFlagMask(std::string identifier) { if (identifier == "normal") return 1; if (identifier == "injured") return 2; LOGWARNING(L"CCmpRangeManager: Invalid flag identifier %hs", identifier.c_str()); return 0; } virtual void SetEntityFlag(entity_id_t ent, std::string identifier, bool value) { std::map::iterator it = m_EntityData.find(ent); // We don't have this entity if (it == m_EntityData.end()) return; u8 flag = GetEntityFlagMask(identifier); // We don't have a flag set if (flag == 0) { LOGWARNING(L"CCmpRangeManager: Invalid flag identifier %hs for entity %u", identifier.c_str(), ent); return; } if (value) it->second.flags |= flag; else it->second.flags &= ~flag; } // **************************************************************** // LOS implementation: virtual CLosQuerier GetLosQuerier(player_id_t player) { if (GetLosRevealAll(player)) return CLosQuerier(0xFFFFFFFFu, m_LosStateRevealed, m_TerrainVerticesPerSide); else return CLosQuerier(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); } virtual ELosVisibility GetLosVisibility(entity_id_t ent, player_id_t player, bool forceRetainInFog) { // (We can't use m_EntityData since this needs to handle LOCAL entities too) // Entities not with positions in the world are never visible CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return VIS_HIDDEN; CFixedVector2D pos = cmpPosition->GetPosition2D(); int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); // Reveal flag makes all positioned entities visible if (GetLosRevealAll(player)) { if (LosIsOffWorld(i, j)) return VIS_HIDDEN; else return VIS_VISIBLE; } // Visible if within a visible region CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); if (los.IsVisible(i, j)) return VIS_VISIBLE; // Fogged if the 'retain in fog' flag is set, and in a non-visible explored region if (los.IsExplored(i, j)) { CmpPtr cmpVision(GetSimContext(), ent); if (forceRetainInFog || (cmpVision && cmpVision->GetRetainInFog())) return VIS_FOGGED; } // Otherwise not visible return VIS_HIDDEN; } virtual void SetLosRevealAll(player_id_t player, bool enabled) { m_LosRevealAll[player] = enabled; } virtual bool GetLosRevealAll(player_id_t player) { std::map::const_iterator it; // Special player value can force reveal-all for every player it = m_LosRevealAll.find(-1); if (it != m_LosRevealAll.end() && it->second) return true; // Otherwise check the player-specific flag it = m_LosRevealAll.find(player); if (it != m_LosRevealAll.end() && it->second) return true; return false; } virtual void SetLosCircular(bool enabled) { m_LosCircular = enabled; ResetDerivedData(false); } virtual bool GetLosCircular() { return m_LosCircular; } virtual void SetSharedLos(player_id_t player, std::vector players) { m_SharedLosMasks[player] = CalcSharedLosMask(players); } virtual u32 GetSharedLosMask(player_id_t player) { std::map::const_iterator it = m_SharedLosMasks.find(player); ENSURE(it != m_SharedLosMasks.end()); return m_SharedLosMasks[player]; } void UpdateTerritoriesLos() { CmpPtr cmpTerritoryManager(GetSimContext(), SYSTEM_ENTITY); if (!cmpTerritoryManager || !cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID)) return; const Grid& grid = cmpTerritoryManager->GetTerritoryGrid(); ENSURE(grid.m_W == m_TerrainVerticesPerSide-1 && grid.m_H == m_TerrainVerticesPerSide-1); // For each tile, if it is owned by a valid player then update the LOS // for every vertex around that tile, to mark them as explored for (u16 j = 0; j < grid.m_H; ++j) { for (u16 i = 0; i < grid.m_W; ++i) { u8 p = grid.get(i, j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK; if (p > 0 && p <= MAX_LOS_PLAYER_ID) { u32 &explored = m_ExploredVertices.at(p); explored += !(m_LosState[i + j*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1)))); m_LosState[i + j*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1))); explored += !(m_LosState[i+1 + j*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1)))); m_LosState[i+1 + j*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1))); explored += !(m_LosState[i + (j+1)*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1)))); m_LosState[i + (j+1)*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1))); explored += !(m_LosState[i+1 + (j+1)*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1)))); m_LosState[i+1 + (j+1)*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1))); } } } } /** * Returns whether the given vertex is outside the normal bounds of the world * (i.e. outside the range of a circular map) */ inline bool LosIsOffWorld(ssize_t i, ssize_t j) { // WARNING: CCmpObstructionManager::Rasterise needs to be kept in sync with this const ssize_t edgeSize = 3; // number of vertexes around the edge that will be off-world if (m_LosCircular) { // With a circular map, vertex is off-world if hypot(i - size/2, j - size/2) >= size/2: ssize_t dist2 = (i - m_TerrainVerticesPerSide/2)*(i - m_TerrainVerticesPerSide/2) + (j - m_TerrainVerticesPerSide/2)*(j - m_TerrainVerticesPerSide/2); ssize_t r = m_TerrainVerticesPerSide/2 - edgeSize + 1; // subtract a bit from the radius to ensure nice // SoD blurring around the edges of the map return (dist2 >= r*r); } else { // With a square map, the outermost edge of the map should be off-world, // so the SoD texture blends out nicely return (i < edgeSize || j < edgeSize || i >= m_TerrainVerticesPerSide-edgeSize || j >= m_TerrainVerticesPerSide-edgeSize); } } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). */ inline void LosAddStripHelper(u8 owner, i32 i0, i32 i1, i32 j, u16* counts) { if (i1 < i0) return; i32 idx0 = j*m_TerrainVerticesPerSide + i0; i32 idx1 = j*m_TerrainVerticesPerSide + i1; u32 &explored = m_ExploredVertices.at(owner); for (i32 idx = idx0; idx <= idx1; ++idx) { // Increasing from zero to non-zero - move from unexplored/explored to visible+explored if (counts[idx] == 0) { i32 i = i0 + idx - idx0; if (!LosIsOffWorld(i, j)) { explored += !(m_LosState[idx] & (LOS_EXPLORED << (2*(owner-1)))); m_LosState[idx] |= ((LOS_VISIBLE | LOS_EXPLORED) << (2*(owner-1))); } } ASSERT(counts[idx] < 65535); counts[idx] = (u16)(counts[idx] + 1); // ignore overflow; the player should never have 64K units } } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). */ inline void LosRemoveStripHelper(u8 owner, i32 i0, i32 i1, i32 j, u16* counts) { if (i1 < i0) return; i32 idx0 = j*m_TerrainVerticesPerSide + i0; i32 idx1 = j*m_TerrainVerticesPerSide + i1; for (i32 idx = idx0; idx <= idx1; ++idx) { ASSERT(counts[idx] > 0); counts[idx] = (u16)(counts[idx] - 1); // Decreasing from non-zero to zero - move from visible+explored to explored if (counts[idx] == 0) { // (If LosIsOffWorld then this is a no-op, so don't bother doing the check) m_LosState[idx] &= ~(LOS_VISIBLE << (2*(owner-1))); } } } /** * Update the LOS state of tiles within a given circular range, * either adding or removing visibility depending on the template parameter. * Assumes owner is in the valid range. */ template void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelper"); std::vector& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: if (counts.empty()) counts.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); u16* countsData = &counts[0]; // Compute the circular region as a series of strips. // Rather than quantise pos to vertexes, we do more precise sub-tile computations // to get smoother behaviour as a unit moves rather than jumping a whole tile // at once. // To avoid the cost of sqrt when computing the outline of the circle, // we loop from the bottom to the top and estimate the width of the current // strip based on the previous strip, then adjust each end of the strip // inwards or outwards until it's the widest that still falls within the circle. // Compute top/bottom coordinates, and clamp to exclude the 1-tile border around the map // (so that we never render the sharp edge of the map) i32 j0 = ((pos.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1 = ((pos.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0clamp = std::max(j0, 1); i32 j1clamp = std::min(j1, m_TerrainVerticesPerSide-2); // Translate world coordinates into fractional tile-space coordinates entity_pos_t x = pos.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y = pos.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; entity_pos_t r2 = r.Square(); // Compute the integers on either side of x i32 xfloor = (x - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil = (x + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); // Initialise the strip (i0, i1) to a rough guess i32 i0 = xfloor; i32 i1 = xceil; for (i32 j = j0clamp; j <= j1clamp; ++j) { // Adjust i0 and i1 to be the outermost values that don't exceed // the circle's radius (i.e. require dy^2 + dx^2 <= r^2). // When moving the points inwards, clamp them to xceil+1 or xfloor-1 // so they don't accidentally shoot off in the wrong direction forever. entity_pos_t dy = entity_pos_t::FromInt(j) - y; entity_pos_t dy2 = dy.Square(); while (dy2 + (entity_pos_t::FromInt(i0-1) - x).Square() <= r2) --i0; while (i0 < xceil && dy2 + (entity_pos_t::FromInt(i0) - x).Square() > r2) ++i0; while (dy2 + (entity_pos_t::FromInt(i1+1) - x).Square() <= r2) ++i1; while (i1 > xfloor && dy2 + (entity_pos_t::FromInt(i1) - x).Square() > r2) --i1; #if DEBUG_RANGE_MANAGER_BOUNDS if (i0 <= i1) { ENSURE(dy2 + (entity_pos_t::FromInt(i0) - x).Square() <= r2); ENSURE(dy2 + (entity_pos_t::FromInt(i1) - x).Square() <= r2); } ENSURE(dy2 + (entity_pos_t::FromInt(i0 - 1) - x).Square() > r2); ENSURE(dy2 + (entity_pos_t::FromInt(i1 + 1) - x).Square() > r2); #endif // Clamp the strip to exclude the 1-tile border, // then add or remove the strip as requested i32 i0clamp = std::max(i0, 1); i32 i1clamp = std::min(i1, m_TerrainVerticesPerSide-2); if (adding) LosAddStripHelper(owner, i0clamp, i1clamp, j, countsData); else LosRemoveStripHelper(owner, i0clamp, i1clamp, j, countsData); } } /** * Update the LOS state of tiles within a given circular range, * by removing visibility around the 'from' position * and then adding visibility around the 'to' position. */ void LosUpdateHelperIncremental(u8 owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelperIncremental"); std::vector& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: if (counts.empty()) counts.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); u16* countsData = &counts[0]; // See comments in LosUpdateHelper. // This does exactly the same, except computing the strips for // both circles simultaneously. // (The idea is that the circles will be heavily overlapping, // so we can compute the difference between the removed/added strips // and only have to touch tiles that have a net change.) i32 j0_from = ((from.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1_from = ((from.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0_to = ((to.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1_to = ((to.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0clamp = std::max(std::min(j0_from, j0_to), 1); i32 j1clamp = std::min(std::max(j1_from, j1_to), m_TerrainVerticesPerSide-2); entity_pos_t x_from = from.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y_from = from.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t x_to = to.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y_to = to.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; entity_pos_t r2 = r.Square(); i32 xfloor_from = (x_from - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil_from = (x_from + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); i32 xfloor_to = (x_to - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil_to = (x_to + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); i32 i0_from = xfloor_from; i32 i1_from = xceil_from; i32 i0_to = xfloor_to; i32 i1_to = xceil_to; for (i32 j = j0clamp; j <= j1clamp; ++j) { entity_pos_t dy_from = entity_pos_t::FromInt(j) - y_from; entity_pos_t dy2_from = dy_from.Square(); while (dy2_from + (entity_pos_t::FromInt(i0_from-1) - x_from).Square() <= r2) --i0_from; while (i0_from < xceil_from && dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() > r2) ++i0_from; while (dy2_from + (entity_pos_t::FromInt(i1_from+1) - x_from).Square() <= r2) ++i1_from; while (i1_from > xfloor_from && dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() > r2) --i1_from; entity_pos_t dy_to = entity_pos_t::FromInt(j) - y_to; entity_pos_t dy2_to = dy_to.Square(); while (dy2_to + (entity_pos_t::FromInt(i0_to-1) - x_to).Square() <= r2) --i0_to; while (i0_to < xceil_to && dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() > r2) ++i0_to; while (dy2_to + (entity_pos_t::FromInt(i1_to+1) - x_to).Square() <= r2) ++i1_to; while (i1_to > xfloor_to && dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() > r2) --i1_to; #if DEBUG_RANGE_MANAGER_BOUNDS if (i0_from <= i1_from) { ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() <= r2); ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() <= r2); } ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from - 1) - x_from).Square() > r2); ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from + 1) - x_from).Square() > r2); if (i0_to <= i1_to) { ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() <= r2); ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() <= r2); } ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to - 1) - x_to).Square() > r2); ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to + 1) - x_to).Square() > r2); #endif // Check whether this strip moved at all if (!(i0_to == i0_from && i1_to == i1_from)) { i32 i0clamp_from = std::max(i0_from, 1); i32 i1clamp_from = std::min(i1_from, m_TerrainVerticesPerSide-2); i32 i0clamp_to = std::max(i0_to, 1); i32 i1clamp_to = std::min(i1_to, m_TerrainVerticesPerSide-2); // Check whether one strip is negative width, // and we can just add/remove the entire other strip if (i1clamp_from < i0clamp_from) { LosAddStripHelper(owner, i0clamp_to, i1clamp_to, j, countsData); } else if (i1clamp_to < i0clamp_to) { LosRemoveStripHelper(owner, i0clamp_from, i1clamp_from, j, countsData); } else { // There are four possible regions of overlap between the two strips // (remove before add, remove after add, add before remove, add after remove). // Process each of the regions as its own strip. // (If this produces negative-width strips then they'll just get ignored // which is fine.) // (If the strips don't actually overlap (which is very rare with normal unit // movement speeds), the region between them will be both added and removed, // so we have to do the add first to avoid overflowing to -1 and triggering // assertion failures.) LosAddStripHelper(owner, i0clamp_to, i0clamp_from-1, j, countsData); LosAddStripHelper(owner, i1clamp_from+1, i1clamp_to, j, countsData); LosRemoveStripHelper(owner, i0clamp_from, i0clamp_to-1, j, countsData); LosRemoveStripHelper(owner, i1clamp_to+1, i1clamp_from, j, countsData); } } } } void LosAdd(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } void LosRemove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } void LosMove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; if ((from - to).CompareLength(visionRange) > 0) { // If it's a very large move, then simply remove and add to the new position LosUpdateHelper((u8)owner, visionRange, from); LosUpdateHelper((u8)owner, visionRange, to); } else { // Otherwise use the version optimised for mostly-overlapping circles LosUpdateHelperIncremental((u8)owner, visionRange, from, to); } } virtual u8 GetPercentMapExplored(player_id_t player) { return m_ExploredVertices.at((u8)player) * 100 / m_TotalInworldVertices; } }; REGISTER_COMPONENT_TYPE(RangeManager) Index: ps/trunk/source/simulation2/components/ICmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.cpp (revision 13625) +++ ps/trunk/source/simulation2/components/ICmpRangeManager.cpp (revision 13626) @@ -1,53 +1,55 @@ /* Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ICmpRangeManager.h" #include "simulation2/system/InterfaceScripted.h" std::string ICmpRangeManager::GetLosVisibility_wrapper(entity_id_t ent, int player, bool forceRetainInFog) { ELosVisibility visibility = GetLosVisibility(ent, player, forceRetainInFog); switch (visibility) { case VIS_HIDDEN: return "hidden"; case VIS_FOGGED: return "fogged"; case VIS_VISIBLE: return "visible"; default: return "error"; // should never happen } } BEGIN_INTERFACE_WRAPPER(RangeManager) DEFINE_INTERFACE_METHOD_5("ExecuteQuery", std::vector, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int) DEFINE_INTERFACE_METHOD_6("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int, u8) +DEFINE_INTERFACE_METHOD_7("CreateActiveParabolicQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveParabolicQuery, entity_id_t, entity_pos_t, entity_pos_t, entity_pos_t, std::vector, int, u8) DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("ResetActiveQuery", std::vector, ICmpRangeManager, ResetActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_3("SetEntityFlag", void, ICmpRangeManager, SetEntityFlag, entity_id_t, std::string, bool) DEFINE_INTERFACE_METHOD_1("GetEntityFlagMask", u8, ICmpRangeManager, GetEntityFlagMask, std::string) DEFINE_INTERFACE_METHOD_1("GetEntitiesByPlayer", std::vector, ICmpRangeManager, GetEntitiesByPlayer, player_id_t) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool) DEFINE_INTERFACE_METHOD_2("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, player_id_t, bool) +DEFINE_INTERFACE_METHOD_5("GetElevationAdaptedRange", entity_pos_t, ICmpRangeManager, GetElevationAdaptedRange, CFixedVector3D, CFixedVector3D, entity_pos_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("GetLosVisibility", std::string, ICmpRangeManager, GetLosVisibility_wrapper, entity_id_t, player_id_t, bool) DEFINE_INTERFACE_METHOD_1("SetLosCircular", void, ICmpRangeManager, SetLosCircular, bool) DEFINE_INTERFACE_METHOD_0("GetLosCircular", bool, ICmpRangeManager, GetLosCircular) DEFINE_INTERFACE_METHOD_2("SetSharedLos", void, ICmpRangeManager, SetSharedLos, player_id_t, std::vector) DEFINE_INTERFACE_METHOD_1("GetPercentMapExplored", u8, ICmpRangeManager, GetPercentMapExplored, player_id_t) END_INTERFACE_WRAPPER(RangeManager) Index: ps/trunk/source/simulation2/components/ICmpRangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.h (revision 13625) +++ ps/trunk/source/simulation2/components/ICmpRangeManager.h (revision 13626) @@ -1,331 +1,359 @@ /* Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_ICMPRANGEMANAGER #define INCLUDED_ICMPRANGEMANAGER +#include "maths/FixedVector3D.h" + #include "simulation2/system/Interface.h" #include "simulation2/helpers/Position.h" #include "simulation2/helpers/Player.h" #include "graphics/Terrain.h" // for TERRAIN_TILE_SIZE /** * Provides efficient range-based queries of the game world, * and also LOS-based effects (fog of war). * * (These are somewhat distinct concepts but they share a lot of the implementation, * so for efficiency they're combined into this class.) * * Possible use cases: * - combat units need to detect targetable enemies entering LOS, so they can choose * to auto-attack. * - auras let a unit have some effect on all units (or those of the same player, or of enemies) * within a certain range. * - capturable animals need to detect when a player-owned unit is nearby and no units of other * players are in range. * - scenario triggers may want to detect when units enter a given area. * - units gathering from a resource that is exhausted need to find a new resource of the * same type, near the old one and reachable. * - projectile weapons with splash damage need to find all units within some distance * of the target point. * - ... * * In most cases the users are event-based and want notifications when something * has entered or left the range, and the query can be set up once and rarely changed. * These queries have to be fast. It's fine to approximate an entity as a point. * * Current design: * * This class handles just the most common parts of range queries: * distance, target interface, and player ownership. * The caller can then apply any more complex filtering that it needs. * * There are two types of query: * Passive queries are performed by ExecuteQuery and immediately return the matching entities. * Active queries are set up by CreateActiveQuery, and then a CMessageRangeUpdate message will be * sent to the entity once per turn if anybody has entered or left the range since the last RangeUpdate. * Queries can be disabled, in which case no message will be sent. */ class ICmpRangeManager : public IComponent { public: /** * External identifiers for active queries. */ typedef u32 tag_t; /** * Set the bounds of the world. * Entities should not be outside the bounds (else efficiency will suffer). * @param x0,z0,x1,z1 Coordinates of the corners of the world * @param vertices Number of terrain vertices per side */ virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) = 0; /** * Execute a passive query. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum distance in metres (inclusive). * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance. * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, std::vector owners, int requiredInterface) = 0; /** * Construct an active query. The query will be disabled by default. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum distance in metres (inclusive). * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance. * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @param flags if a entity in range has one of the flags set it will show up. * @return unique non-zero identifier of query. */ virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, std::vector owners, int requiredInterface, u8 flags) = 0; + /** + * Construct an active query of a paraboloic form around the unit. + * The query will be disabled by default. + * @param source the entity around which the range will be computed. + * @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks. + * @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation; + * or -1.0 to ignore distance. + * For units on a different elevation, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them + * @param elevationBonus extra bonus so the source can be placed higher and shoot further + * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. + * @param requiredInterface if non-zero, an interface ID that matching entities must implement. + * @param flags if a entity in range has one of the flags set it will show up. + * @return unique non-zero identifier of query. + */ + virtual tag_t CreateActiveParabolicQuery(entity_id_t source, + entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, std::vector owners, int requiredInterface, u8 flags) = 0; + + + /** + * Get the average elevation over 8 points on distance range around the entity + * @param id the entity id to look around + * @param range the distance to compare terrain height with + * @return a fixed number representing the average difference. It's positive when the entity is on average higher than the terrain surrounding it. + */ + virtual entity_pos_t GetElevationAdaptedRange(CFixedVector3D pos, CFixedVector3D rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) = 0; + /** * Destroy a query and clean up resources. This must be called when an entity no longer needs its * query (e.g. when the entity is destroyed). * @param tag identifier of query. */ virtual void DestroyActiveQuery(tag_t tag) = 0; /** * Re-enable the processing of a query. * @param tag identifier of query. */ virtual void EnableActiveQuery(tag_t tag) = 0; /** * Disable the processing of a query (no RangeUpdate messages will be sent). * @param tag identifier of query. */ virtual void DisableActiveQuery(tag_t tag) = 0; /** * Immediately execute a query, and re-enable it if disabled. * The next RangeUpdate message will say who has entered/left since this call, * so you won't miss any notifications. * @param tag identifier of query. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ResetActiveQuery(tag_t tag) = 0; /** * Returns list of all entities for specific player. * (This is on this interface because it shares a lot of the implementation. * Maybe it should be extended to be more like ExecuteQuery without * the range parameter.) */ virtual std::vector GetEntitiesByPlayer(player_id_t player) = 0; /** * Toggle the rendering of debug info. */ virtual void SetDebugOverlay(bool enabled) = 0; /** * Returns the mask for the specified identifier. */ virtual u8 GetEntityFlagMask(std::string identifier) = 0; /** * Set the flag specified by the identifier to the supplied value for the entity * @param ent the entity whose flags will be modified. * @param identifier the flag to be modified. * @param value to which the flag will be set. */ virtual void SetEntityFlag(entity_id_t ent, std::string identifier, bool value) = 0; // LOS interface: enum ELosState { LOS_UNEXPLORED = 0, LOS_EXPLORED = 1, LOS_VISIBLE = 2, LOS_MASK = 3 }; enum ELosVisibility { VIS_HIDDEN, VIS_FOGGED, VIS_VISIBLE }; /** * Object providing efficient abstracted access to the LOS state. * This depends on some implementation details of CCmpRangeManager. * * This *ignores* the GetLosRevealAll flag - callers should check that explicitly. */ class CLosQuerier { private: friend class CCmpRangeManager; friend class TestLOSTexture; CLosQuerier(u32 playerMask, const std::vector& data, ssize_t verticesPerSide) : m_Data(&data[0]), m_PlayerMask(playerMask), m_VerticesPerSide(verticesPerSide) { } const CLosQuerier& operator=(const CLosQuerier&); // not implemented public: /** * Returns whether the given vertex is visible (i.e. is within a unit's LOS). */ inline bool IsVisible(ssize_t i, ssize_t j) { if (!(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide)) return false; // Check high bit of each bit-pair if ((m_Data[j*m_VerticesPerSide + i] & m_PlayerMask) & 0xAAAAAAAAu) return true; else return false; } /** * Returns whether the given vertex is explored (i.e. was (or still is) within a unit's LOS). */ inline bool IsExplored(ssize_t i, ssize_t j) { if (!(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide)) return false; // Check low bit of each bit-pair if ((m_Data[j*m_VerticesPerSide + i] & m_PlayerMask) & 0x55555555u) return true; else return false; } /** * Returns whether the given vertex is visible (i.e. is within a unit's LOS). * i and j must be in the range [0, verticesPerSide), else behaviour is undefined. */ inline bool IsVisible_UncheckedRange(ssize_t i, ssize_t j) { #ifndef NDEBUG ENSURE(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide); #endif // Check high bit of each bit-pair if ((m_Data[j*m_VerticesPerSide + i] & m_PlayerMask) & 0xAAAAAAAAu) return true; else return false; } /** * Returns whether the given vertex is explored (i.e. was (or still is) within a unit's LOS). * i and j must be in the range [0, verticesPerSide), else behaviour is undefined. */ inline bool IsExplored_UncheckedRange(ssize_t i, ssize_t j) { #ifndef NDEBUG ENSURE(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide); #endif // Check low bit of each bit-pair if ((m_Data[j*m_VerticesPerSide + i] & m_PlayerMask) & 0x55555555u) return true; else return false; } private: u32 m_PlayerMask; const u32* m_Data; ssize_t m_VerticesPerSide; }; /** * Returns a CLosQuerier for checking whether vertex positions are visible to the given player * (or other players it shares LOS with). */ virtual CLosQuerier GetLosQuerier(player_id_t player) = 0; /** * Returns the visibility status of the given entity, with respect to the given player. * Returns VIS_HIDDEN if the entity doesn't exist or is not in the world. * This respects the GetLosRevealAll flag. * If forceRetainInFog is true, the visibility acts as if CCmpVision's RetainInFog flag were set. * TODO: This is a hack to allow preview entities in FoW to return fogged instead of hidden, * see http://trac.wildfiregames.com/ticket/958 */ virtual ELosVisibility GetLosVisibility(entity_id_t ent, player_id_t player, bool forceRetainInFog = false) = 0; /** * GetLosVisibility wrapped for script calls. * Returns "hidden", "fogged" or "visible". */ std::string GetLosVisibility_wrapper(entity_id_t ent, player_id_t player, bool forceRetainInFog); /** * Set whether the whole map should be made visible to the given player. * If player is -1, the map will be made visible to all players. */ virtual void SetLosRevealAll(player_id_t player, bool enabled) = 0; /** * Returns whether the whole map has been made visible to the given player. */ virtual bool GetLosRevealAll(player_id_t player) = 0; /** * Set the LOS to be restricted to a circular map. */ virtual void SetLosCircular(bool enabled) = 0; /** * Returns whether the LOS is restricted to a circular map. */ virtual bool GetLosCircular() = 0; /** * Sets shared LOS data for player to the given list of players. */ virtual void SetSharedLos(player_id_t player, std::vector players) = 0; /** * Returns shared LOS mask for player. */ virtual u32 GetSharedLosMask(player_id_t player) = 0; /** * Get percent map explored statistics for specified player. */ virtual u8 GetPercentMapExplored(player_id_t player) = 0; /** * Perform some internal consistency checks for testing/debugging. */ virtual void Verify() = 0; DECLARE_INTERFACE_TYPE(RangeManager) }; #endif // INCLUDED_ICMPRANGEMANAGER Index: ps/trunk/source/simulation2/system/InterfaceScripted.h =================================================================== --- ps/trunk/source/simulation2/system/InterfaceScripted.h (revision 13625) +++ ps/trunk/source/simulation2/system/InterfaceScripted.h (revision 13626) @@ -1,88 +1,94 @@ /* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_INTERFACE_SCRIPTED #define INCLUDED_INTERFACE_SCRIPTED #include "scriptinterface/ScriptInterface.h" #include "js/jsapi.h" #define BEGIN_INTERFACE_WRAPPER(iname) \ JSClass class_ICmp##iname = { \ "ICmp" #iname, JSCLASS_HAS_PRIVATE, \ JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_StrictPropertyStub, \ JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub, \ JSCLASS_NO_OPTIONAL_MEMBERS \ }; \ static JSFunctionSpec methods_ICmp##iname[] = { #define END_INTERFACE_WRAPPER(iname) \ { NULL } \ }; \ void ICmp##iname::InterfaceInit(ScriptInterface& scriptInterface) { \ JSContext* cx = scriptInterface.GetContext(); \ JSObject* global = JS_GetGlobalObject(cx); \ JS_InitClass(cx, global, NULL, &class_ICmp##iname, NULL, 0, NULL, methods_ICmp##iname, NULL, NULL); \ } \ JSClass* ICmp##iname::GetJSClass() const { return &class_ICmp##iname; } \ void RegisterComponentInterface_##iname(ScriptInterface& scriptInterface) { \ ICmp##iname::InterfaceInit(scriptInterface); \ } #define DEFINE_INTERFACE_METHOD_0(scriptname, rettype, classname, methodname) \ { scriptname, \ ScriptInterface::callMethod, \ 0, \ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, #define DEFINE_INTERFACE_METHOD_1(scriptname, rettype, classname, methodname, arg1) \ { scriptname, \ ScriptInterface::callMethod, \ 1, \ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, #define DEFINE_INTERFACE_METHOD_2(scriptname, rettype, classname, methodname, arg1, arg2) \ { scriptname, \ ScriptInterface::callMethod, \ 2, \ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, #define DEFINE_INTERFACE_METHOD_3(scriptname, rettype, classname, methodname, arg1, arg2, arg3) \ { scriptname, \ ScriptInterface::callMethod, \ 3, \ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, #define DEFINE_INTERFACE_METHOD_4(scriptname, rettype, classname, methodname, arg1, arg2, arg3, arg4) \ { scriptname, \ ScriptInterface::callMethod, \ 4, \ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, #define DEFINE_INTERFACE_METHOD_5(scriptname, rettype, classname, methodname, arg1, arg2, arg3, arg4, arg5) \ { scriptname, \ ScriptInterface::callMethod, \ 5, \ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, #define DEFINE_INTERFACE_METHOD_6(scriptname, rettype, classname, methodname, arg1, arg2, arg3, arg4, arg5, arg6) \ { scriptname, \ ScriptInterface::callMethod, \ 6, \ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, +#define DEFINE_INTERFACE_METHOD_7(scriptname, rettype, classname, methodname, arg1, arg2, arg3, arg4, arg5, arg6, arg7) \ + { scriptname, \ + ScriptInterface::callMethod, \ + 7, \ + JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, + #endif // INCLUDED_INTERFACE_SCRIPTED