Index: ps/trunk/binaries/data/mods/public/gui/page_session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/page_session.xml (revision 14416)
+++ ps/trunk/binaries/data/mods/public/gui/page_session.xml (revision 14417)
@@ -1,16 +1,20 @@
common/setup.xmlcommon/styles.xmlcommon/sprite1.xmlcommon/icon_sprites.xmlcommon/common_sprites.xmlcommon/common_styles.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+ common/modern/setup.xml
+
session/sprites.xmlsession/setup.xmlsession/styles.xmlsession/session.xmlcommon/global.xml
Index: ps/trunk/binaries/data/mods/public/gui/session/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 14417)
@@ -1,2220 +1,2223 @@
const SDL_BUTTON_LEFT = 1;
const SDL_BUTTON_MIDDLE = 2;
const SDL_BUTTON_RIGHT = 3;
const SDLK_LEFTBRACKET = 91;
const SDLK_RIGHTBRACKET = 93;
const SDLK_RSHIFT = 303;
const SDLK_LSHIFT = 304;
const SDLK_RCTRL = 305;
const SDLK_LCTRL = 306;
const SDLK_RALT = 307;
const SDLK_LALT = 308;
// TODO: these constants should be defined somewhere else instead, in
// case any other code wants to use them too
const ACTION_NONE = 0;
const ACTION_GARRISON = 1;
const ACTION_REPAIR = 2;
const ACTION_GUARD = 3;
var preSelectedAction = ACTION_NONE;
const INPUT_NORMAL = 0;
const INPUT_SELECTING = 1;
const INPUT_BANDBOXING = 2;
const INPUT_BUILDING_PLACEMENT = 3;
const INPUT_BUILDING_CLICK = 4;
const INPUT_BUILDING_DRAG = 5;
const INPUT_BATCHTRAINING = 6;
const INPUT_PRESELECTEDACTION = 7;
const INPUT_BUILDING_WALL_CLICK = 8;
const INPUT_BUILDING_WALL_PATHING = 9;
const INPUT_MASSTRIBUTING = 10;
var inputState = INPUT_NORMAL;
var 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;
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 (!target)
{
if (action == "set-rallypoint")
{
var cursor = "";
var data = {command: "walk"};
if (Engine.HotkeyIsPressed("session.attackmove"))
{
data = {command: "attack-walk"};
cursor = "action-attack-move";
}
return {"possible": true, "data": data, "cursor": cursor};
}
else if (action == "move" || action == "attack-move")
return {"possible": true};
else if (action == "remove-guard")
return {"possible": true};
else
return {"possible": false};
}
if (action == "unset-rallypoint" && selection.indexOf(target) != -1)
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 = GetExtendedEntityState(target);
var gaiaOwned = (targetState.player == 0);
// Look to see what type of command units going to the rally point should use
if (action == "set-rallypoint")
{
// We assume that all entities are owned by the same player (given par entState of 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 tooltip;
// default to walking there (or attack-walking if hotkey pressed)
var data = {command: "walk"};
var cursor = "";
if (Engine.HotkeyIsPressed("session.attackmove"))
{
data = {command: "attack-walk"};
cursor = "action-attack-move";
}
if (targetState.garrisonHolder && (playerOwned || mutualAllyOwned))
{
data.command = "garrison";
data.target = target;
cursor = "action-garrison";
tooltip = "Current garrison: " + targetState.garrisonHolder.garrisonedEntitiesCount
+ "/" + targetState.garrisonHolder.capacity;
if (targetState.garrisonHolder.garrisonedEntitiesCount >= targetState.garrisonHolder.capacity)
tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
}
else if (targetState.resourceSupply)
{
var resourceType = targetState.resourceSupply.type;
if (resourceType.generic == "treasure")
cursor = "action-gather-" + resourceType.generic;
else
cursor = "action-gather-" + resourceType.specific;
data.command = "gather";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
}
else if (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);
+ tooltip += "\nGain: " + getTradingTooltip(gain);
else // Foundation or cannot produce traders
- tooltip += "\nExpected gain (metal): " + getTradingTooltip(gain);
+ tooltip += "\nExpected gain: " + getTradingTooltip(gain);
}
}
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities
if (!Engine.HotkeyIsPressed("session.autorallypoint") || !targetState.production || !targetState.production.entities.length)
{
for (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 = GetExtendedEntityState(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];
// 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.garrisonedEntitiesCount
+ "/" + targetState.garrisonHolder.capacity;
var extraCount = 0;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.garrisonedEntitiesCount;
if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
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);
+ tooltip += "\nGain: " + 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);
+ tooltip = "Destination trade market.\nGain: " + 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);
+ tooltip = "Right-click to set as destination trade market.\nGain: " + 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;
case "guard":
if (targetState.guard && (playerOwned || mutualAllyOwned))
return {"possible": (entState.unitAI && entState.unitAI.canGuard && !(targetState.unitAI && targetState.unitAI.isGuarding))};
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 at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
// Work out whether at least part the selection have rally points
// while none have UnitAI
var haveRallyPoints = !haveUnitAI && selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.rallyPoint;
});
var targets = [];
var target = 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;
case ACTION_GUARD:
if (getActionInfo("guard", target).possible)
return {"type": "guard", "cursor": "action-guard", "target": target};
else
return {"type": "none", "cursor": "action-guard-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 (haveUnitAI && Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible)
{
return {"type": "attack-move", "cursor": "action-attack-move"};
}
else if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("guard", target).possible)
{
return {"type": "guard", "cursor": "action-guard", "target": target};
}
else if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("remove-guard", target).possible)
{
var isGuarding = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI && entState.unitAI.isGuarding;
});
if (isGuarding)
return {"type": "remove-guard", "cursor": "action-remove-guard"};
}
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 (haveRallyPoints && (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 (haveRallyPoints && getActionInfo("unset-rallypoint", target).possible)
return {"type": "unset-rallypoint"};
else if (haveUnitAI && getActionInfo("move", target).possible)
return {"type": "move"};
}
return {"type": "none", "cursor": "", "target": target};
}
var dragStart; // used for remembering mouse coordinates at start of drag operations
function tryPlaceBuilding(queued)
{
if (placementSupport.mode !== "building")
{
error("[tryPlaceBuilding] Called while in '"+placementSupport.mode+"' placement mode instead of 'building'");
return false;
}
// Use the preview to check it's a valid build location
if (!updateBuildingPlacementPreview())
{
// invalid location - don't build it
// TODO: play a sound?
return false;
}
var selection = g_Selection.toList();
// Start the construction
Engine.PostNetworkCommand({
"type": "construct",
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed,
"entities": selection,
"autorepair": true,
"autocontinue": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
if (!queued)
placementSupport.Reset();
else
placementSupport.RandomizeActorSeed();
return true;
}
function tryPlaceWall(queued)
{
if (placementSupport.mode !== "wall")
{
error("[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();
}
if (ev.hotkey == "session.highlightguarding")
{
g_ShowGuarding = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
if (ev.hotkey == "session.highlightguarded")
{
g_ShowGuarded = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
// State-machine processing:
switch (inputState)
{
case INPUT_NORMAL:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
if (ents.length)
g_Selection.setHighlightList([ents[0]]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
dragStart = [ ev.x, ev.y ];
inputState = INPUT_SELECTING;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
return doAction(action, ev);
}
break;
case "hotkeydown":
if (ev.hotkey.indexOf("selection.group.") == 0)
{
var now = new Date();
if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey))
{
if (ev.hotkey.indexOf("selection.group.select.") == 0)
{
var sptr = ev.hotkey.split(".");
performGroup("snap", sptr[3]);
}
}
else
{
var sptr = ev.hotkey.split(".");
performGroup(sptr[2], sptr[3]);
doublePressTimer = now.getTime();
prevHotkey = ev.hotkey;
}
}
break;
}
break;
case INPUT_PRESELECTEDACTION:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
if (ents.length)
g_Selection.setHighlightList([ents[0]]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
return doAction(action, ev);
}
else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
// else
default:
// Slight hack: If selection is empty, reset the input state
if (g_Selection.toList().length == 0)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved further than a limit, switch to bandbox mode
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
inputState = INPUT_BANDBOXING;
return false;
}
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
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 "guard":
Engine.PostNetworkCommand({"type": "guard", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] });
return true;
case "remove-guard":
Engine.PostNetworkCommand({"type": "remove-guard", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "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, "queued": queued});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
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)
+// Called by GUI when user changes required trading goods
+function selectRequiredGoods(data)
{
- Engine.PostNetworkCommand({"type": "select-trading-goods", "entities": data.entities, "preferredGoods": data.preferredGoods});
+ Engine.PostNetworkCommand({"type": "select-required-goods", "entities": data.entities, "requiredGoods": data.requiredGoods});
}
// Called by GUI when user clicks exchange resources button
function exchangeResources(command)
{
Engine.PostNetworkCommand({"type": "barter", "sell": command.sell, "buy": command.buy, "amount": command.amount});
}
// Camera jumping: when the user presses a hotkey the current camera location is marked.
// When they press another hotkey the camera jumps back to that position. If the camera is already roughly at that location,
// jump back to where it was previously.
var jumpCameraPositions = [], jumpCameraLast;
function jumpCamera(index)
{
var position = jumpCameraPositions[index], distanceThreshold = Engine.ConfigDB_GetValue("user", "camerajump.threshold");
if (position)
{
if (jumpCameraLast &&
Math.abs(Engine.CameraGetX() - position.x) < distanceThreshold &&
Math.abs(Engine.CameraGetZ() - position.z) < distanceThreshold)
Engine.CameraMoveTo(jumpCameraLast.x, jumpCameraLast.z);
else
{
jumpCameraLast = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
Engine.CameraMoveTo(position.x, position.z);
}
}
}
function setJumpCamera(index)
{
jumpCameraPositions[index] = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
}
// Batch training:
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
// When the user releases shift, or clicks on a different training button, we create the batched units
var batchTrainingEntities;
var batchTrainingType;
var batchTrainingCount;
var batchTrainingEntityAllowedCount;
const batchIncrementSize = 5;
function flushTrainingBatch()
{
var appropriateBuildings = getBuildingsWhichCanTrainEntity(batchTrainingEntities, batchTrainingType);
// If training limits don't allow us to train batchTrainingCount in each appropriate building
if (batchTrainingEntityAllowedCount !== undefined &&
batchTrainingEntityAllowedCount < batchTrainingCount * appropriateBuildings.length)
{
// Train as many full batches as we can
var buildingsCountToTrainFullBatch = Math.floor(batchTrainingEntityAllowedCount / batchTrainingCount);
var buildingsToTrainFullBatch = appropriateBuildings.slice(0, buildingsCountToTrainFullBatch);
Engine.PostNetworkCommand({"type": "train", "entities": buildingsToTrainFullBatch,
"template": batchTrainingType, "count": batchTrainingCount});
// Train remainer in one more building
var remainderToTrain = batchTrainingEntityAllowedCount % batchTrainingCount;
Engine.PostNetworkCommand({"type": "train",
"entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ],
"template": batchTrainingType, "count": remainderToTrain});
}
else
{
Engine.PostNetworkCommand({"type": "train", "entities": appropriateBuildings,
"template": batchTrainingType, "count": batchTrainingCount});
}
}
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(function(entity) {
var state = GetEntityState(entity);
var canTrain = state && state.production && state.production.entities.length &&
state.production.entities.indexOf(trainEntType) != -1;
return canTrain;
});
}
function getEntityLimitAndCount(playerState, entType)
{
var template = GetTemplateData(entType);
var entCategory = null;
if (template.trainingRestrictions)
entCategory = template.trainingRestrictions.category;
else if (template.buildRestrictions)
entCategory = template.buildRestrictions.category;
var entLimit = undefined;
var entCount = undefined;
var entLimitChangers = undefined;
var canBeAddedCount = undefined;
if (entCategory && playerState.entityLimits[entCategory] != null)
{
entLimit = playerState.entityLimits[entCategory];
entCount = playerState.entityCounts[entCategory];
entLimitChangers = playerState.entityLimitChangers[entCategory];
canBeAddedCount = Math.max(entLimit - entCount, 0);
}
return [entLimit, entCount, canBeAddedCount, entLimitChangers];
}
// Add the unit shown at position to the training queue for all entities in the selection
function addTrainingByPosition(position)
{
var simState = GetSimState();
var playerState = simState.players[Engine.GetPlayerID()];
var selection = g_Selection.toList();
if (!selection.length)
return;
var trainableEnts = getAllTrainableEntitiesFromSelection();
// Check if the position is valid
if (!trainableEnts.length || trainableEnts.length <= position)
return;
var entToTrain = trainableEnts[position];
addTrainingToQueue(selection, entToTrain, playerState);
return;
}
// Called by GUI when user clicks training button
function addTrainingToQueue(selection, trainEntType, playerState)
{
// Create list of buildings which can train trainEntType
var appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
// Check trainEntType entity limit and count
var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType)
// 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 = GetExtendedEntityState(entity);
var playerID = Engine.GetPlayerID();
if (entState.player == playerID || g_DevSettings.controlAll)
{
switch (commandName)
{
case "delete":
var selection = g_Selection.toList();
if (selection.length > 0)
if (!entState.resourceSupply || !entState.resourceSupply.killBeforeGather)
openDeleteDialog(selection);
break;
case "stop":
var selection = g_Selection.toList();
if (selection.length > 0)
stopUnits(selection);
break;
case "garrison":
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
break;
case "repair":
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
break;
case "add-guard":
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
break;
case "remove-guard":
removeGuard();
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;
case "back-to-work":
backToWork();
break;
case "increase-alert-level":
increaseAlertLevel();
break;
case "alert-end":
endOfAlert();
break;
+ case "select-trading-goods":
+ toggleTrade();
+ 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 backToWork()
{
// Filter out all entities that can't go back to work.
var workers = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
return (state && state.unitAI && state.unitAI.hasWorkOrders);
});
Engine.PostNetworkCommand({"type": "back-to-work", "entities": workers});
}
function removeGuard()
{
// Filter out all entities that are currently guarding/escorting.
var entities = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
return (state && state.unitAI && state.unitAI.isGuarding);
});
Engine.PostNetworkCommand({"type": "remove-guard", "entities": entities});
}
function increaseAlertLevel()
{
var raisers = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
return (state && state.alertRaiser && state.alertRaiser.canIncreaseLevel);
});
Engine.PostNetworkCommand({"type": "increase-alert-level", "entities": raisers});
}
function endOfAlert()
{
var raisers = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
return (state && state.alertRaiser && state.alertRaiser.hasRaisedAlert);
});
Engine.PostNetworkCommand({"type": "alert-end", "entities": raisers});
}
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/menu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 14417)
@@ -1,447 +1,594 @@
const PAUSE = "Pause";
const RESUME = "Resume";
/*
* MENU POSITION CONSTANTS
*/
// Menu / panel border size
const MARGIN = 4;
// Includes the main menu button
const NUM_BUTTONS = 8;
// Regular menu buttons
const BUTTON_HEIGHT = 32;
// The position where the bottom of the menu will end up (currently 228)
const END_MENU_POSITION = (BUTTON_HEIGHT * NUM_BUTTONS) + MARGIN;
// Menu starting position: bottom
const MENU_BOTTOM = 0;
// Menu starting position: top
const MENU_TOP = MENU_BOTTOM - END_MENU_POSITION;
// Menu starting position: overall
const INITIAL_MENU_POSITION = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM;
// Number of pixels per millisecond to move
const MENU_SPEED = 1.2;
+// Trade menu: available resources and step for probability changes
+const RESOURCES = ["food", "wood", "stone", "metal"];
+const STEP = 5;
+
var isMenuOpen = false;
var menu;
var isDiplomacyOpen = false;
+var isTradeOpen = false;
// Redefined every time someone makes a tribute (so we can save some data in a closure). Called in input.js handleInputBeforeGui.
var flushTributing = function() {};
// Ignore size defined in XML and set the actual menu size here
function initMenuPosition()
{
menu = getGUIObjectByName("menu");
menu.size = INITIAL_MENU_POSITION;
}
// =============================================================================
// Overall Menu
// =============================================================================
//
// Slide menu
function updateMenuPosition(dt)
{
if (isMenuOpen)
{
var maxOffset = END_MENU_POSITION - menu.size.bottom;
if (maxOffset > 0)
{
var offset = Math.min(MENU_SPEED * dt, maxOffset);
var size = menu.size;
size.top += offset;
size.bottom += offset;
menu.size = size;
}
}
else
{
var maxOffset = menu.size.top - MENU_TOP;
if (maxOffset > 0)
{
var offset = Math.min(MENU_SPEED * dt, maxOffset);
var size = menu.size;
size.top -= offset;
size.bottom -= offset;
menu.size = size;
}
}
}
// Opens the menu by revealing the screen which contains the menu
function openMenu()
{
// playButtonSound();
isMenuOpen = true;
}
// Closes the menu and resets position
function closeMenu()
{
// playButtonSound();
isMenuOpen = false;
}
function toggleMenu()
{
if (isMenuOpen == true)
closeMenu();
else
openMenu();
}
// Menu buttons
// =============================================================================
function settingsMenuButton()
{
closeMenu();
closeOpenDialogs();
openSettings(true);
}
function chatMenuButton()
{
closeMenu();
closeOpenDialogs();
openChat();
}
function diplomacyMenuButton()
{
closeMenu();
closeOpenDialogs();
openDiplomacy();
}
function pauseMenuButton()
{
togglePause();
}
function resignMenuButton()
{
closeMenu();
closeOpenDialogs();
pauseGame();
var btCaptions = ["Yes", "No"];
var btCode = [resignGame, resumeGame];
messageBox(400, 200, "Are you sure you want to resign?", "Confirmation", 0, btCaptions, btCode);
}
function exitMenuButton()
{
closeMenu();
closeOpenDialogs();
pauseGame();
var btCaptions = ["Yes", "No"];
var btCode = [leaveGame, resumeGame];
messageBox(400, 200, "Are you sure you want to quit?", "Confirmation", 0, btCaptions, btCode);
}
function openDeleteDialog(selection)
{
closeMenu();
closeOpenDialogs();
var deleteSelectedEntities = function ()
{
Engine.PostNetworkCommand({"type": "delete-entities", "entities": selection});
};
var btCaptions = ["Yes", "No"];
var btCode = [deleteSelectedEntities, resumeGame];
messageBox(400, 200, "Destroy everything currently selected?", "Delete", 0, btCaptions, btCode);
}
// Menu functions
// =============================================================================
function openSave()
{
closeMenu();
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_savegame.xml", {"gameDataCallback": getSavedGameData, "closeCallback": resumeGame});
}
function openSettings(pause)
{
getGUIObjectByName("settingsDialogPanel").hidden = false;
if (pause)
pauseGame();
}
function closeSettings(resume)
{
getGUIObjectByName("settingsDialogPanel").hidden = true;
if (resume)
resumeGame();
}
function openChat()
{
getGUIObjectByName("chatInput").focus(); // Grant focus to the input area
getGUIObjectByName("chatDialogPanel").hidden = false;
}
function closeChat()
{
getGUIObjectByName("chatInput").caption = ""; // Clear chat input
getGUIObjectByName("chatInput").blur(); // Remove focus
getGUIObjectByName("chatDialogPanel").hidden = true;
}
function toggleChatWindow(teamChat)
{
closeSettings();
var chatWindow = getGUIObjectByName("chatDialogPanel");
var chatInput = getGUIObjectByName("chatInput");
if (chatWindow.hidden)
chatInput.focus(); // Grant focus to the input area
else
{
if (chatInput.caption.length)
{
submitChatInput();
return;
}
chatInput.caption = ""; // Clear chat input
}
getGUIObjectByName("toggleTeamChat").checked = teamChat;
chatWindow.hidden = !chatWindow.hidden;
}
function setDiplomacy(data)
{
Engine.PostNetworkCommand({"type": "diplomacy", "to": data.to, "player": data.player});
}
function tributeResource(data)
{
Engine.PostNetworkCommand({"type": "tribute", "player": data.player, "amounts": data.amounts});
}
function openDiplomacy()
{
+ if (isTradeOpen)
+ closeTrade();
isDiplomacyOpen = true;
var we = Engine.GetPlayerID();
var players = getPlayerData(g_PlayerAssignments);
// Get offset for one line
var onesize = getGUIObjectByName("diplomacyPlayer[0]").size;
var rowsize = onesize.bottom - onesize.top;
// We don't include gaia
for (var i = 1; i < players.length; i++)
{
// Apply offset
var row = getGUIObjectByName("diplomacyPlayer["+(i-1)+"]");
var size = row.size;
size.top = rowsize*(i-1);
size.bottom = rowsize*i;
row.size = size;
// Set background colour
var playerColor = players[i].color.r+" "+players[i].color.g+" "+players[i].color.b;
row.sprite = "colour: "+playerColor + " 32";
getGUIObjectByName("diplomacyPlayerName["+(i-1)+"]").caption = "[color=\"" + playerColor + "\"]" + players[i].name + "[/color]";
getGUIObjectByName("diplomacyPlayerCiv["+(i-1)+"]").caption = g_CivData[players[i].civ].Name;
getGUIObjectByName("diplomacyPlayerTeam["+(i-1)+"]").caption = (players[i].team < 0) ? "None" : players[i].team+1;
if (i != we)
getGUIObjectByName("diplomacyPlayerTheirs["+(i-1)+"]").caption = (players[i].isAlly[we] ? "Ally" : (players[i].isNeutral[we] ? "Neutral" : "Enemy"));
// Don't display the options for ourself, or if we or the other player aren't active anymore
if (i == we || players[we].state != "active" || players[i].state != "active")
{
// Hide the unused/unselectable options
for each (var a in ["TributeFood", "TributeWood", "TributeStone", "TributeMetal", "Ally", "Neutral", "Enemy"])
getGUIObjectByName("diplomacyPlayer"+a+"["+(i-1)+"]").hidden = true;
continue;
}
// Tribute
for each (var resource in ["food", "wood", "stone", "metal"])
{
var button = getGUIObjectByName("diplomacyPlayerTribute"+toTitleCase(resource)+"["+(i-1)+"]");
button.onpress = (function(player, resource, button){
// Implement something like how unit batch training works. Shift+click to send 500, shift+click+click to send 1000, etc.
// Also see input.js (searching for "INPUT_MASSTRIBUTING" should get all the relevant parts).
var multiplier = 1;
return function() {
var isBatchTrainPressed = Engine.HotkeyIsPressed("session.masstribute");
if (isBatchTrainPressed)
{
inputState = INPUT_MASSTRIBUTING;
multiplier += multiplier == 1 ? 4 : 5;
}
var amounts = {
"food": (resource == "food" ? 100 : 0) * multiplier,
"wood": (resource == "wood" ? 100 : 0) * multiplier,
"stone": (resource == "stone" ? 100 : 0) * multiplier,
"metal": (resource == "metal" ? 100 : 0) * multiplier,
};
button.tooltip = formatTributeTooltip(players[player], resource, amounts[resource]);
// This is in a closure so that we have access to `player`, `amounts`, and `multiplier` without some
// evil global variable hackery.
flushTributing = function() {
tributeResource({"player": player, "amounts": amounts});
multiplier = 1;
button.tooltip = formatTributeTooltip(players[player], resource, 100);
};
if (!isBatchTrainPressed)
flushTributing();
};
})(i, resource, button);
button.hidden = false;
button.tooltip = formatTributeTooltip(players[i], resource, 100);
}
// Skip our own teams on teams locked
if (players[we].teamsLocked && players[we].team != -1 && players[we].team == players[i].team)
continue;
// Diplomacy settings
// Set up the buttons
for each (var setting in ["ally", "neutral", "enemy"])
{
var button = getGUIObjectByName("diplomacyPlayer"+toTitleCase(setting)+"["+(i-1)+"]");
if (setting == "ally")
{
if (players[we].isAlly[i])
button.caption = "x";
else
button.caption = "";
}
else if (setting == "neutral")
{
if (players[we].isNeutral[i])
button.caption = "x";
else
button.caption = "";
}
else // "enemy"
{
if (players[we].isEnemy[i])
button.caption = "x";
else
button.caption = "";
}
button.onpress = (function(e){ return function() { setDiplomacy(e) } })({"player": i, "to": setting});
button.hidden = false;
}
}
getGUIObjectByName("diplomacyDialogPanel").hidden = false;
}
function closeDiplomacy()
{
isDiplomacyOpen = false;
getGUIObjectByName("diplomacyDialogPanel").hidden = true;
}
function toggleDiplomacy()
{
if (isDiplomacyOpen)
closeDiplomacy();
else
openDiplomacy();
};
+function openTrade()
+{
+ if (isDiplomacyOpen)
+ closeDiplomacy();
+ isTradeOpen = true;
+
+ var updateButtons = function()
+ {
+ for (var res in button)
+ {
+ button[res].label.caption = proba[res] + "%";
+ if (res == selec)
+ {
+ button[res].res.enabled = false;
+ button[res].sel.hidden = false;
+ button[res].up.hidden = true;
+ button[res].dn.hidden = true;
+ }
+ else
+ {
+ button[res].res.enabled = true;
+ button[res].sel.hidden = true;
+ button[res].up.hidden = (proba[res] == 100 || proba[selec] == 0);
+ button[res].dn.hidden = (proba[res] == 0 || proba[selec] == 100);
+ }
+ }
+ }
+
+ var proba = Engine.GuiInterfaceCall("GetTradingGoods");
+ var button = {};
+ var selec = RESOURCES[0];
+ for (var i = 0; i < RESOURCES.length; ++i)
+ {
+ var buttonResource = getGUIObjectByName("tradeResource["+i+"]");
+ if (i > 0)
+ {
+ var size = getGUIObjectByName("tradeResource["+(i-1)+"]").size;
+ var width = size.right - size.left;
+ size.left += width;
+ size.right += width;
+ getGUIObjectByName("tradeResource["+i+"]").size = size;
+ }
+ var resource = RESOURCES[i];
+ proba[resource] = (proba[resource] ? proba[resource] : 0);
+ var buttonResource = getGUIObjectByName("tradeResourceButton["+i+"]");
+ var icon = getGUIObjectByName("tradeResourceIcon["+i+"]");
+ icon.sprite = "stretched:session/icons/resources/" + resource + ".png";
+ var label = getGUIObjectByName("tradeResourceText["+i+"]");
+ var buttonUp = getGUIObjectByName("tradeArrowUp["+i+"]");
+ var buttonDn = getGUIObjectByName("tradeArrowDn["+i+"]");
+ var iconSel = getGUIObjectByName("tradeResourceSelection["+i+"]");
+ button[resource] = { "res": buttonResource, "up": buttonUp, "dn": buttonDn, "label": label, "sel": iconSel };
+
+ buttonResource.onpress = (function(resource){
+ return function() {
+ if (selec == resource)
+ return;
+ selec = resource;
+ updateButtons();
+ }
+ })(resource);
+
+ buttonUp.onpress = (function(resource){
+ return function() {
+ proba[resource] += Math.min(STEP, proba[selec]);
+ proba[selec] -= Math.min(STEP, proba[selec]);
+ Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba});
+ updateButtons();
+ }
+ })(resource);
+
+ buttonDn.onpress = (function(resource){
+ return function() {
+ proba[selec] += Math.min(STEP, proba[resource]);
+ proba[resource] -= Math.min(STEP, proba[resource]);
+ Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba});
+ updateButtons();
+ }
+ })(resource);
+ }
+ updateButtons();
+
+ var traderNumber = Engine.GuiInterfaceCall("GetTraderNumber");
+ var caption = "";
+ var comma = "";
+ if (traderNumber.landTrader.total == 0)
+ caption += "0";
+ else
+ {
+ if (traderNumber.landTrader.trading > 0)
+ {
+ caption += traderNumber.landTrader.trading + " trading"
+ comma = ", ";
+ }
+ if (traderNumber.landTrader.garrisoned > 0)
+ {
+ caption += comma + traderNumber.landTrader.garrisoned + " garrisoned inside ships";
+ comma = ", ";
+ }
+ var inactive = traderNumber.landTrader.total - traderNumber.landTrader.trading - traderNumber.landTrader.garrisoned;
+ if (inactive > 0)
+ caption += comma + "[color=\"orange\"]" + inactive + " inactive[/color]";
+ }
+ getGUIObjectByName("landTraders").caption = caption;
+
+ caption = "";
+ comma = "";
+ if (traderNumber.shipTrader.total == 0)
+ caption += "0";
+ else
+ {
+ if (traderNumber.shipTrader.trading > 0)
+ {
+ caption += traderNumber.shipTrader.trading + " trading"
+ comma = ", ";
+ }
+ var inactive = traderNumber.shipTrader.total - traderNumber.shipTrader.trading;
+ if (inactive > 0)
+ caption += comma + "[color=\"orange\"]" + inactive + " inactive[/color]";
+ }
+ getGUIObjectByName("shipTraders").caption = caption;
+
+ getGUIObjectByName("tradeDialogPanel").hidden = false;
+}
+
+function closeTrade()
+{
+ isTradeOpen = false;
+ getGUIObjectByName("tradeDialogPanel").hidden = true;
+}
+
+function toggleTrade()
+{
+ if (isTradeOpen)
+ closeTrade();
+ else
+ openTrade();
+};
+
function toggleGameSpeed()
{
var gameSpeed = getGUIObjectByName("gameSpeed");
gameSpeed.hidden = !gameSpeed.hidden;
}
function pauseGame()
{
getGUIObjectByName("pauseButtonText").caption = RESUME;
getGUIObjectByName("pauseOverlay").hidden = false;
setPaused(true);
}
function resumeGame()
{
getGUIObjectByName("pauseButtonText").caption = PAUSE;
getGUIObjectByName("pauseOverlay").hidden = true;
setPaused(false);
}
function togglePause()
{
closeMenu();
closeOpenDialogs();
var pauseOverlay = getGUIObjectByName("pauseOverlay");
if (pauseOverlay.hidden)
{
getGUIObjectByName("pauseButtonText").caption = RESUME;
setPaused(true);
}
else
{
setPaused(false);
getGUIObjectByName("pauseButtonText").caption = PAUSE;
}
pauseOverlay.hidden = !pauseOverlay.hidden;
}
function openManual()
{
closeMenu();
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_manual.xml", {"page": "intro", "closeCallback": resumeGame});
}
function toggleDeveloperOverlay()
{
if (Engine.HasXmppClient() && Engine.IsRankedGame())
return;
var devCommands = getGUIObjectByName("devCommands");
var text = devCommands.hidden ? "opened." : "closed.";
submitChatDirectly("The Developer Overlay was " + text);
// Update the options dialog
getGUIObjectByName("developerOverlayCheckbox").checked = devCommands.hidden;
devCommands.hidden = !devCommands.hidden;
}
function closeOpenDialogs()
{
closeMenu();
closeChat();
closeDiplomacy();
+ closeTrade();
closeSettings(false);
}
function formatTributeTooltip(player, resource, amount)
{
var playerColor = player.color.r + " " + player.color.g + " " + player.color.b;
return "Tribute " + amount + " " + resource + " to [color=\"" + playerColor + "\"]" + player.name +
"[/color]. Shift-click to tribute " + (amount < 500 ? 500 : amount + 500) + ".";
}
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 14417)
@@ -1,895 +1,893 @@
// Network Mode
var g_IsNetworked = false;
// Is this user in control of game settings (i.e. is a network server, or offline player)
var g_IsController;
// Match ID for tracking
var g_MatchID;
// Cache the basic player data (name, civ, color)
var g_Players = [];
// Cache the useful civ data
var g_CivData = {};
var g_GameSpeeds = {};
var g_CurrentSpeed;
var g_PlayerAssignments = { "local": { "name": "You", "player": 1 } };
// Cache dev-mode settings that are frequently or widely used
var g_DevSettings = {
controlAll: false
};
// Whether status bars should be shown for all of the player's units.
var g_ShowAllStatusBars = false;
// Indicate when one of the current player's training queues is blocked
// (this is used to support population counter blinking)
var g_IsTrainingBlocked = false;
// Cache simulation state (updated on every simulation update)
var g_SimState;
// Cache EntityStates
var g_EntityStates = {}; // {id:entState}
// Whether the player has lost/won and reached the end of their game
var g_GameEnded = false;
var g_Disconnected = false; // Lost connection to server
// Holds player states from the last tick
var g_CachedLastStates = "";
// Colors to flash when pop limit reached
const DEFAULT_POPULATION_COLOR = "white";
const POPULATION_ALERT_COLOR = "orange";
// List of additional entities to highlight
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
function GetSimState()
{
if (!g_SimState)
- {
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
- }
return g_SimState;
}
function GetEntityState(entId)
{
if (!(entId in g_EntityStates))
{
var entState = Engine.GuiInterfaceCall("GetEntityState", entId);
if (entState)
entState.extended = false;
g_EntityStates[entId] = entState;
}
return g_EntityStates[entId];
}
function GetExtendedEntityState(entId)
{
if (entId in g_EntityStates)
var entState = g_EntityStates[entId];
else
var entState = Engine.GuiInterfaceCall("GetEntityState", entId);
if (!entState || entState.extended)
return entState;
var extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId);
for (var prop in extension)
entState[prop] = extension[prop];
entState.extended = true;
g_EntityStates[entId] = entState;
return entState;
}
// Cache TemplateData
var g_TemplateData = {}; // {id:template}
function GetTemplateData(templateName)
{
if (!(templateName in g_TemplateData))
{
var template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
g_TemplateData[templateName] = template;
}
return g_TemplateData[templateName];
}
// Cache TechnologyData
var g_TechnologyData = {}; // {id:template}
function GetTechnologyData(technologyName)
{
if (!(technologyName in g_TechnologyData))
{
var template = Engine.GuiInterfaceCall("GetTechnologyData", technologyName);
g_TechnologyData[technologyName] = template;
}
return g_TechnologyData[technologyName];
}
// Init
function init(initData, hotloadData)
{
if (initData)
{
g_IsNetworked = initData.isNetworked; // Set network mode
g_IsController = initData.isController; // Set controller mode
g_PlayerAssignments = initData.playerAssignments;
g_MatchID = initData.attribs.matchID;
// Cache the player data
// (This may be updated at runtime by handleNetMessage)
g_Players = getPlayerData(g_PlayerAssignments);
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
getGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
}
else // Needed for autostart loading option
{
g_Players = getPlayerData(null);
}
// Cache civ data
g_CivData = loadCivData();
g_CivData["gaia"] = { "Code": "gaia", "Name": "Gaia" };
g_GameSpeeds = initGameSpeeds();
g_CurrentSpeed = Engine.GetSimRate();
var gameSpeed = getGUIObjectByName("gameSpeed");
gameSpeed.list = g_GameSpeeds.names;
gameSpeed.list_data = g_GameSpeeds.speeds;
var idx = g_GameSpeeds.speeds.indexOf(g_CurrentSpeed);
gameSpeed.selected = idx != -1 ? idx : g_GameSpeeds["default"];
gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); }
getGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[Engine.GetPlayerID()].civ].Emblem;
getGUIObjectByName("civIcon").tooltip = g_CivData[g_Players[Engine.GetPlayerID()].civ].Name;
initMenuPosition(); // set initial position
// Populate player selection dropdown
var playerNames = [];
var playerIDs = [];
for (var player in g_Players)
{
playerNames.push(g_Players[player].name);
playerIDs.push(player);
}
var viewPlayerDropdown = getGUIObjectByName("viewPlayer");
viewPlayerDropdown.list = playerNames;
viewPlayerDropdown.list_data = playerIDs;
viewPlayerDropdown.selected = Engine.GetPlayerID();
// If in Atlas editor, disable the exit button
if (Engine.IsAtlasRunning())
getGUIObjectByName("menuExitButton").enabled = false;
if (hotloadData)
{
g_Selection.selected = hotloadData.selection;
}
else
{
// Starting for the first time:
var civMusic = g_CivData[g_Players[Engine.GetPlayerID()].civ].Music;
initMusic();
global.music.storeTracks(civMusic);
global.music.setState(global.music.states.PEACE);
playRandomAmbient("temperate");
}
if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true")
getGUIObjectByName("timeElapsedCounter").hidden = false;
onSimulationUpdate();
// Report the performance after 5 seconds (when we're still near
// the initial camera view) and a minute (when the profiler will
// have settled down if framerates as very low), to give some
// extremely rough indications of performance
setTimeout(function() { reportPerformance(5); }, 5000);
setTimeout(function() { reportPerformance(60); }, 60000);
}
function selectViewPlayer(playerID)
{
Engine.SetPlayerID(playerID);
if (playerID != 0) {
getGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[playerID].civ].Emblem;
getGUIObjectByName("civIcon").tooltip = g_CivData[g_Players[playerID].civ].Name;
}
}
function reportPerformance(time)
{
var settings = Engine.GetMapSettings();
var data = {
time: time,
map: settings.Name,
seed: settings.Seed, // only defined for random maps
size: settings.Size, // only defined for random maps
profiler: Engine.GetProfilerState()
};
Engine.SubmitUserReport("profile", 3, JSON.stringify(data));
}
function resignGame()
{
var simState = GetSimState();
// Players can't resign if they've already won or lost.
if (simState.players[Engine.GetPlayerID()].state != "active" || g_Disconnected)
return;
// Tell other players that we have given up and been defeated
Engine.PostNetworkCommand({
"type": "defeat-player",
"playerId": Engine.GetPlayerID()
});
global.music.setState(global.music.states.DEFEAT);
resumeGame();
}
function leaveGame()
{
var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
var playerState = extendedSimState.players[Engine.GetPlayerID()];
var mapSettings = Engine.GetMapSettings();
var gameResult;
if (g_Disconnected)
{
gameResult = "You have been disconnected."
}
else if (playerState.state == "won")
{
gameResult = "You have won the battle!";
}
else if (playerState.state == "defeated")
{
gameResult = "You have been defeated...";
}
else // "active"
{
gameResult = "You have abandoned the game.";
// Tell other players that we have given up and been defeated
Engine.PostNetworkCommand({
"type": "defeat-player",
"playerId": Engine.GetPlayerID()
});
global.music.setState(global.music.states.DEFEAT);
}
stopAmbient();
endGame();
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_summary.xml", {
"gameResult" : gameResult,
"timeElapsed" : extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"players": g_Players,
"mapSettings": mapSettings
});
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return { selection: g_Selection.selected };
}
// Return some data that will be stored in saved game files
function getSavedGameData()
{
var data = {};
data.playerAssignments = g_PlayerAssignments;
data.groups = g_Groups.groups;
// TODO: any other gui state?
return data;
}
function restoreSavedGameData(data)
{
// Clear selection when loading a game
g_Selection.reset();
// Restore control groups
for (var groupNumber in data.groups)
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
}
updateGroups();
}
var lastTickTime = new Date;
/**
* Called every frame.
*/
function onTick()
{
var now = new Date;
var tickLength = new Date - lastTickTime;
lastTickTime = now;
checkPlayerState();
while (true)
{
var message = Engine.PollNetworkClient();
if (!message)
break;
handleNetMessage(message);
}
updateCursorAndTooltip();
// If the selection changed, we need to regenerate the sim display (the display depends on both the
// simulation state and the current selection).
if (g_Selection.dirty)
{
g_Selection.dirty = false;
onSimulationUpdate();
// Display rally points for selected buildings
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
// Run timers
updateTimers();
// Animate menu
updateMenuPosition(tickLength);
// When training is blocked, flash population (alternates colour every 500msec)
if (g_IsTrainingBlocked && (Date.now() % 1000) < 500)
getGUIObjectByName("resourcePop").textcolor = POPULATION_ALERT_COLOR;
else
getGUIObjectByName("resourcePop").textcolor = DEFAULT_POPULATION_COLOR;
// Clear renamed entities list
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function checkPlayerState()
{
// Once the game ends, we're done here.
if (g_GameEnded)
return;
// Send a game report for each player in this game.
var m_simState = GetSimState();
var playerState = m_simState.players[Engine.GetPlayerID()];
var tempStates = "";
for each (var player in m_simState.players) {tempStates += player.state + ",";}
if (g_CachedLastStates != tempStates)
{
g_CachedLastStates = tempStates;
reportGame(Engine.GuiInterfaceCall("GetExtendedSimulationState"));
}
// If the local player hasn't finished playing, we return here to avoid the victory/defeat messages.
if (playerState.state == "active")
return;
// We can't resign once the game is over.
getGUIObjectByName("menuResignButton").enabled = false;
// Make sure nothing is open to avoid stacking.
closeMenu();
closeOpenDialogs();
// Make sure this doesn't run again.
g_GameEnded = true;
if (Engine.IsAtlasRunning())
{
// If we're in Atlas, we can't leave the game
var btCaptions = ["OK"];
var btCode = [null];
var message = "Press OK to continue";
}
else
{
var btCaptions = ["Yes", "No"];
var btCode = [leaveGame, null];
var message = "Do you want to quit?";
}
if (playerState.state == "defeated")
{
global.music.setState(global.music.states.DEFEAT);
messageBox(400, 200, message, "DEFEATED!", 0, btCaptions, btCode);
}
else if (playerState.state == "won")
{
global.music.setState(global.music.states.VICTORY);
// TODO: Reveal map directly instead of this silly proxy.
if (!getGUIObjectByName("devCommandsRevealMap").checked)
getGUIObjectByName("devCommandsRevealMap").checked = true;
messageBox(400, 200, message, "VICTORIOUS!", 0, btCaptions, btCode);
}
}
function changeGameSpeed(speed)
{
// For non-networked games only
if (!g_IsNetworked)
{
Engine.SetSimRate(speed);
g_CurrentSpeed = speed;
}
}
/**
* Recomputes GUI state that depends on simulation state or selection state. Called directly every simulation
* update (see session.xml), or from onTick when the selection has changed.
*/
function onSimulationUpdate()
{
g_EntityStates = {};
g_TemplateData = {};
g_TechnologyData = {};
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
// If we're called during init when the game is first loading, there will be no simulation yet, so do nothing
if (!g_SimState)
return;
handleNotifications();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updateHero();
updateGroups();
updateDebug();
updatePlayerDisplay();
updateSelectionDetails();
updateResearchDisplay();
updateBuildingPlacementPreview();
updateTimeElapsedCounter();
// Update music state on basis of battle state.
var battleState = Engine.GuiInterfaceCall("GetBattleState", Engine.GetPlayerID());
if (battleState)
global.music.setState(global.music.states[battleState]);
}
function updateHero()
{
var simState = GetSimState();
var playerState = simState.players[Engine.GetPlayerID()];
var heroButton = getGUIObjectByName("unitHeroButton");
if (!playerState || playerState.heroes.length <= 0)
{
heroButton.hidden = true;
return;
}
var heroImage = getGUIObjectByName("unitHeroImage");
var heroState = GetExtendedEntityState(playerState.heroes[0]);
var template = GetTemplateData(heroState.template);
heroImage.sprite = "stretched:session/portraits/" + template.icon;
var hero = playerState.heroes[0];
heroButton.onpress = function()
{
if (!Engine.HotkeyIsPressed("selection.add"))
g_Selection.reset();
g_Selection.addList([hero]);
};
heroButton.ondoublepress = function() { selectAndMoveTo(getEntityOrHolder(hero)); };
heroButton.hidden = false;
// Setup tooltip
var tooltip = "[font=\"serif-bold-16\"]" + template.name.specific + "[/font]";
tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + heroState.hitpoints + "/" + heroState.maxHitpoints;
tooltip += "\n[font=\"serif-bold-13\"]" + (heroState.attack ? heroState.attack.type + " " : "")
+ "Attack:[/font] " + damageTypeDetails(heroState.attack);
// Show max attack range if ranged attack, also convert to tiles (4m per tile)
if (heroState.attack && heroState.attack.type == "Ranged")
tooltip += ", [font=\"serif-bold-13\"]Range:[/font] " + Math.round(heroState.attack.maxRange/4);
tooltip += "\n[font=\"serif-bold-13\"]Armor:[/font] " + damageTypeDetails(heroState.armour);
tooltip += "\n" + template.tooltip;
heroButton.tooltip = tooltip;
};
function updateGroups()
{
var guiName = "Group";
g_Groups.update();
for (var i = 0; i < 10; i++)
{
var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]");
var label = getGUIObjectByName("unit"+guiName+"Label["+i+"]").caption = i;
if (g_Groups.groups[i].getTotalCount() == 0)
button.hidden = true;
else
button.hidden = false;
button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); } })(i);
button.ondoublepress = (function(i) { return function() { performGroup("snap", i); } })(i);
}
var numButtons = i;
var rowLength = 1;
var numRows = Math.ceil(numButtons / rowLength);
var buttonSideLength = getGUIObjectByName("unit"+guiName+"Button[0]").size.bottom;
var buttonSpacer = buttonSideLength+1;
for (var i = 0; i < numRows; i++)
layoutButtonRow(i, guiName, buttonSideLength, buttonSpacer, rowLength*i, rowLength*(i+1) );
}
function updateDebug()
{
var simState = GetSimState();
var debug = getGUIObjectByName("debug");
if (getGUIObjectByName("devDisplayState").checked)
{
debug.hidden = false;
}
else
{
debug.hidden = true;
return;
}
var conciseSimState = deepcopy(simState);
conciseSimState.players = "<<>>";
var text = "simulation: " + uneval(conciseSimState);
var selection = g_Selection.toList();
if (selection.length)
{
var entState = GetExtendedEntityState(selection[0]);
if (entState)
{
var template = GetTemplateData(entState.template);
text += "\n\nentity: {\n";
for (var k in entState)
text += " "+k+":"+uneval(entState[k])+"\n";
text += "}\n\ntemplate: " + uneval(template);
}
}
debug.caption = text;
}
function updatePlayerDisplay()
{
var simState = GetSimState();
var playerState = simState.players[Engine.GetPlayerID()];
if (!playerState)
return;
getGUIObjectByName("resourceFood").caption = playerState.resourceCounts.food;
getGUIObjectByName("resourceWood").caption = playerState.resourceCounts.wood;
getGUIObjectByName("resourceStone").caption = playerState.resourceCounts.stone;
getGUIObjectByName("resourceMetal").caption = playerState.resourceCounts.metal;
getGUIObjectByName("resourcePop").caption = playerState.popCount + "/" + playerState.popLimit;
g_IsTrainingBlocked = playerState.trainingBlocked;
}
function selectAndMoveTo(ent)
{
var entState = GetEntityState(ent);
if (!entState || !entState.position)
return;
g_Selection.reset();
g_Selection.addList([ent]);
var position = entState.position;
Engine.CameraMoveTo(position.x, position.z);
}
function updateResearchDisplay()
{
var researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", Engine.GetPlayerID());
if (!researchStarted)
return;
// Set up initial positioning.
var buttonSideLength = getGUIObjectByName("researchStartedButton[0]").size.right;
for (var i = 0; i < 10; ++i)
{
var button = getGUIObjectByName("researchStartedButton[" + i + "]");
var size = button.size;
size.top = (4 + buttonSideLength) * i;
size.bottom = size.top + buttonSideLength;
button.size = size;
}
var numButtons = 0;
for (var tech in researchStarted)
{
// Show at most 10 in-progress techs.
if (numButtons >= 10)
break;
var template = GetTechnologyData(tech);
var button = getGUIObjectByName("researchStartedButton[" + numButtons + "]");
button.hidden = false;
button.tooltip = getEntityNames(template);
button.onpress = (function(e) { return function() { selectAndMoveTo(e) } })(researchStarted[tech].researcher);
var icon = "stretched:session/portraits/" + template.icon;
getGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon;
// Scale the progress indicator.
var size = getGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left));
getGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size;
++numButtons;
}
// Hide unused buttons.
for (var i = numButtons; i < 10; ++i)
getGUIObjectByName("researchStartedButton[" + i + "]").hidden = true;
}
function updateTimeElapsedCounter()
{
var simState = GetSimState();
var speed = g_CurrentSpeed != 1.0 ? " (" + g_CurrentSpeed + "x)" : "";
var timeElapsedCounter = getGUIObjectByName("timeElapsedCounter");
timeElapsedCounter.caption = timeToString(simState.timeElapsed) + speed;
}
// Toggles the display of status bars for all of the player's entities.
function recalculateStatusBarDisplay()
{
if (g_ShowAllStatusBars)
var entities = Engine.PickFriendlyEntitiesOnScreen(Engine.GetPlayerID());
else
{
var selected = g_Selection.toList();
for each (var ent in g_Selection.highlighted)
selected.push(ent);
// Remove selected entities from the 'all entities' array, to avoid disabling their status bars.
var entities = Engine.GuiInterfaceCall("GetPlayerEntities").filter(
function(idx) { return (selected.indexOf(idx) == -1); }
);
}
Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars });
}
// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
var entsAdd = []; // list of entities units to be highlighted
var entsRemove = [];
var highlighted = g_Selection.toList();
for each (var ent in g_Selection.highlighted)
highlighted.push(ent);
if (g_ShowGuarding)
{
// flag the guarding entities to add in this additional highlight
for each (var sel in g_Selection.selected)
{
var state = GetEntityState(sel);
if (!state.guard || !state.guard.entities.length)
continue;
for each (var ent in state.guard.entities)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
}
if (g_ShowGuarded)
{
// flag the guarded entities to add in this additional highlight
for each (var sel in g_Selection.selected)
{
var state = GetEntityState(sel);
if (!state.unitAI || !state.unitAI.isGuarding)
continue;
var ent = state.unitAI.isGuarding;
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
}
// flag the entities to remove (from the previously added) from this additional highlight
for each (var ent in g_AdditionalHighlight)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
entsRemove.push(ent);
_setHighlight(entsAdd , HIGHLIGHTED_ALPHA, true );
_setHighlight(entsRemove, 0 , false);
g_AdditionalHighlight = entsAdd;
}
// Temporarily adding this here
const AMBIENT_TEMPERATE = "temperate";
var currentAmbient;
function playRandomAmbient(type)
{
switch (type)
{
case AMBIENT_TEMPERATE:
// Seem to need the underscore at the end of "temperate" to avoid crash
// (Might be caused by trying to randomly load day_temperate.xml)
// currentAmbient = newRandomSound("ambient", "temperate_", "dayscape");
const AMBIENT = "audio/ambient/dayscape/day_temperate_gen_03.ogg";
Engine.PlayAmbientSound( AMBIENT, true );
break;
default:
Engine.Console_Write("Unrecognized ambient type: " + type);
break;
}
}
// Temporarily adding this here
function stopAmbient()
{
if (currentAmbient)
{
currentAmbient.free();
currentAmbient = null;
}
}
// Send a report on the game status to the lobby
function reportGame(extendedSimState)
{
if (!Engine.HasXmppClient())
return;
// Resources gathered and used
var playerFoodGatheredString = "";
var playerWoodGatheredString = "";
var playerStoneGatheredString = "";
var playerMetalGatheredString = "";
var playerFoodUsedString = "";
var playerWoodUsedString = "";
var playerStoneUsedString = "";
var playerMetalUsedString = "";
// Resources exchanged
var playerFoodBoughtString = "";
var playerWoodBoughtString = "";
var playerStoneBoughtString = "";
var playerMetalBoughtString = "";
var playerFoodSoldString = "";
var playerWoodSoldString = "";
var playerStoneSoldString = "";
var playerMetalSoldString = "";
var playerTradeIncomeString = "";
// Unit Stats
var playerUnitsLostString = "";
var playerUnitsTrainedString = "";
var playerEnemyUnitsKilledString = "";
// Building stats
var playerBuildingsConstructedString = "";
var playerBuildingsLostString = "";
var playerEnemyBuildingsDestroyedString = "";
var playerCivCentersBuiltString = "";
var playerEnemyCivCentersDestroyedString = "";
// Tribute
var playerTributeSentString = "";
var playerTributeReceivedString = "";
// Various
var mapName = Engine.GetMapSettings().Name;
var playerStatesString = "";
var playerCivsString = "";
var playerPercentMapExploredString = "";
var playerTreasuresCollectedString = "";
// Serialize the statistics for each player into a comma-separated list.
for each (var player in extendedSimState.players)
{
playerStatesString += player.state + ",";
playerCivsString += player.civ + ",";
playerFoodGatheredString += player.statistics.resourcesGathered.food + ",";
playerWoodGatheredString += player.statistics.resourcesGathered.wood + ",";
playerStoneGatheredString += player.statistics.resourcesGathered.stone + ",";
playerMetalGatheredString += player.statistics.resourcesGathered.metal + ",";
playerFoodUsedString += player.statistics.resourcesUsed.food + ",";
playerWoodUsedString += player.statistics.resourcesUsed.wood + ",";
playerStoneUsedString += player.statistics.resourcesUsed.stone + ",";
playerMetalUsedString += player.statistics.resourcesUsed.metal + ",";
playerUnitsLostString += player.statistics.unitsLost + ",";
playerUnitsTrainedString += player.statistics.unitsTrained + ",";
playerEnemyUnitsKilledString += player.statistics.enemyUnitsKilled + ",";
playerBuildingsConstructedString += player.statistics.buildingsConstructed + ",";
playerBuildingsLostString += player.statistics.buildingsLost + ",";
playerEnemyBuildingsDestroyedString += player.statistics.enemyBuildingsDestroyed + ",";
playerFoodBoughtString += player.statistics.resourcesBought.food + ",";
playerWoodBoughtString += player.statistics.resourcesBought.wood + ",";
playerStoneBoughtString += player.statistics.resourcesBought.stone + ",";
playerMetalBoughtString += player.statistics.resourcesBought.metal + ",";
playerFoodSoldString += player.statistics.resourcesSold.food + ",";
playerWoodSoldString += player.statistics.resourcesSold.wood + ",";
playerStoneSoldString += player.statistics.resourcesSold.stone + ",";
playerMetalSoldString += player.statistics.resourcesSold.metal + ",";
playerTributeSentString += player.statistics.tributesSent + ",";
playerTributeReceivedString += player.statistics.tributesReceived + ",";
playerPercentMapExploredString += player.statistics.precentMapExplored = ",";
playerCivCentersBuiltString += player.statistics.civCentresBuilt + ",";
playerEnemyCivCentersDestroyedString += player.statistics.enemyCivCentresDestroyed + ",";
playerTreasuresCollectedString += player.statistics.treasuresCollected + ",";
playerTradeIncomeString += player.statistics.tradeIncome + ",";
}
// Send the report with serialized data
Engine.SendGameReport({
"timeElapsed" : extendedSimState.timeElapsed,
"playerStates" : playerStatesString,
"playerID": Engine.GetPlayerID(),
"matchID": g_MatchID,
"civs" : playerCivsString,
"mapName" : mapName,
"foodGathered": playerFoodGatheredString,
"woodGathered": playerWoodGatheredString,
"stoneGathered": playerStoneGatheredString,
"metalGathered": playerMetalGatheredString,
"foodUsed": playerFoodUsedString,
"woodUsed": playerWoodUsedString,
"stoneUsed": playerStoneUsedString,
"metalUsed": playerMetalUsedString,
"unitsLost": playerUnitsLostString,
"unitsTrained": playerUnitsTrainedString,
"enemyUnitsKilled": playerEnemyUnitsKilledString,
"buildingsLost": playerBuildingsLostString,
"buildingsConstructed": playerBuildingsConstructedString,
"enemyBuildingsDestroyed": playerEnemyBuildingsDestroyedString,
"foodBought": playerFoodBoughtString,
"woodBought": playerWoodBoughtString,
"stoneBought": playerStoneBoughtString,
"metalBought": playerMetalBoughtString,
"foodSold": playerFoodSoldString,
"woodSold": playerWoodSoldString,
"stoneSold": playerStoneSoldString,
"metalSold": playerMetalSoldString,
"tributeSent": playerTributeSentString,
"tributeReceived": playerTributeReceivedString,
"precentMapExplored": playerPercentMapExploredString,
"civCentersBuilt": playerCivCentersBuiltString,
"enemyCivCentersDestroyed": playerEnemyCivCentersDestroyedString,
"treasuresCollected": playerTreasuresCollectedString,
"tradeIncome": playerTradeIncomeString
});
}
Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 14416)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 14417)
@@ -1,1262 +1,1329 @@
ExitleaveGame()
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 14417)
@@ -1,1322 +1,1325 @@
// Panel types
const SELECTION = "Selection";
const QUEUE = "Queue";
const GARRISON = "Garrison";
const FORMATION = "Formation";
const TRAINING = "Training";
const RESEARCH = "Research";
const CONSTRUCTION = "Construction";
const TRADING = "Trading";
const COMMAND = "Command";
const STANCE = "Stance";
const GATE = "Gate";
const PACK = "Pack";
// Constants
const COMMANDS_PANEL_WIDTH = 228;
const UNIT_PANEL_BASE = -52; // QUEUE: The offset above the main panel (will often be negative)
const UNIT_PANEL_HEIGHT = 44; // QUEUE: The height needed for a row of buttons
// Trading constants
const TRADING_RESOURCES = ["food", "wood", "stone", "metal"];
// Barter constants
const BARTER_RESOURCE_AMOUNT_TO_SELL = 100;
const BARTER_BUNCH_MULTIPLIER = 5;
const BARTER_RESOURCES = ["food", "wood", "stone", "metal"];
const BARTER_ACTIONS = ["Sell", "Buy"];
// Gate constants
const GATE_ACTIONS = ["Lock", "Unlock"];
// The number of currently visible buttons (used to optimise showing/hiding)
var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Research": 0, "Barter": 0, "Trading": 0, "Construction": 0, "Command": 0, "Stance": 0, "Gate": 0, "Pack": 0};
// Unit panels are panels with row(s) of buttons
var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Training", "Barter", "Trading", "Construction", "Research", "Stance", "Command", "Gate", "Pack"];
// Indexes of resources to sell and buy on barter panel
var g_barterSell = 0;
// Lay out a row of centered buttons (does not work inside a loop like the other function)
function layoutButtonRowCentered(rowNumber, guiName, startIndex, endIndex, width)
{
var buttonSideLength = getGUIObjectByName("unit"+guiName+"Button[0]").size.bottom;
var buttonSpacer = buttonSideLength+1;
var colNumber = 0;
// Collect buttons
var buttons = [];
var icons = [];
for (var i = startIndex; i < endIndex; i++)
{
var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]");
var icon = getGUIObjectByName("unit"+guiName+"Icon["+i+"]");
if (button)
{
buttons.push(button);
icons.push(icon);
}
}
// Location of middle button
var middleIndex = Math.ceil(buttons.length/2);
// Determine whether even or odd number of buttons
var center = (buttons.length/2 == Math.ceil(buttons.length/2))? Math.ceil(width/2) : Math.ceil(width/2+buttonSpacer/2);
// Left Side
for (var i = middleIndex-1; i >= 0; i--)
{
if (buttons[i])
{
var icon = icons[i];
var size = buttons[i].size;
size.left = center - buttonSpacer*colNumber - buttonSideLength;
size.right = center - buttonSpacer*colNumber;
size.top = buttonSpacer*rowNumber;
size.bottom = buttonSpacer*rowNumber + buttonSideLength;
buttons[i].size = size;
colNumber++;
}
}
// Right Side
center += 1; // add spacing to center buttons
colNumber = 0; // reset to 0
for (var i = middleIndex; i < buttons.length; i++)
{
if (buttons[i])
{
var icon = icons[i];
var size = buttons[i].size;
size.left = center + buttonSpacer*colNumber;
size.right = center + buttonSpacer*colNumber + buttonSideLength;
size.top = buttonSpacer*rowNumber;
size.bottom = buttonSpacer*rowNumber + buttonSideLength;
buttons[i].size = size;
colNumber++;
}
}
}
// Lay out button rows
function layoutButtonRow(rowNumber, guiName, buttonSideWidth, buttonSpacer, startIndex, endIndex)
{
layoutRow("Button", rowNumber, guiName, buttonSideWidth, buttonSpacer, buttonSideWidth, buttonSpacer, startIndex, endIndex);
}
// Lay out rows
function layoutRow(objectName, rowNumber, guiName, objectSideWidth, objectSpacerWidth, objectSideHeight, objectSpacerHeight, startIndex, endIndex)
{
var colNumber = 0;
for (var i = startIndex; i < endIndex; i++)
{
var button = getGUIObjectByName("unit"+guiName+objectName+"["+i+"]");
if (button)
{
var size = button.size;
size.left = objectSpacerWidth*colNumber;
size.right = objectSpacerWidth*colNumber + objectSideWidth;
size.top = objectSpacerHeight*rowNumber;
size.bottom = objectSpacerHeight*rowNumber + objectSideHeight;
button.size = size;
colNumber++;
}
}
}
// Set the visibility of the object
function setOverlay(object, value)
{
object.hidden = !value;
}
/**
* Format entity count/limit message for the tooltip
*/
function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
{
if (trainEntLimit == undefined)
return "";
var text = "\n\nCurrent Count: " + trainEntCount + ", Limit: " + trainEntLimit + ".";
if (trainEntCount >= trainEntLimit)
text = "[color=\"red\"]" + text + "[/color]";
for (var c in trainEntLimitChangers)
{
if (trainEntLimitChangers[c] > 0)
text += "\n" + c + " enlarges the limit with " + trainEntLimitChangers[c] + ".";
else if (trainEntLimitChangers[c] < 0)
text += "\n" + c + " lessens the limit with " + (-trainEntLimitChangers[c]) + ".";
}
return text;
}
/**
* Format batch training string for the tooltip
* Examples:
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 5"
* buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 10 (2*5)"
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12:
* "Shift-click to train 27 (15 + 12)"
*/
function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
// Don't show the batch training tooltip if either units of this type can't be trained at all
// or only one unit can be trained
if (totalBatchTrainingCount < 2)
return "";
var batchTrainingString = "";
var fullBatchesString = "";
if (buildingsCountToTrainFullBatch > 0)
{
if (buildingsCountToTrainFullBatch > 1)
fullBatchesString += buildingsCountToTrainFullBatch + "*";
fullBatchesString += fullBatchSize;
}
var remainderBatchString = remainderBatch > 0 ? remainderBatch : "";
var batchDetailsString = "";
// We need to display the batch details part if there is either more than
// one building with full batch or one building with the full batch and
// another with a partial batch
if (buildingsCountToTrainFullBatch > 1 ||
(buildingsCountToTrainFullBatch == 1 && remainderBatch > 0))
{
batchDetailsString += " (" + fullBatchesString;
if (remainderBatchString != "")
batchDetailsString += " + " + remainderBatchString;
batchDetailsString += ")";
}
return "\n[font=\"serif-bold-13\"]Shift-click[/font][font=\"serif-13\"] to train "
+ totalBatchTrainingCount + batchDetailsString + ".[/font]";
}
/**
* Helper function for updateUnitCommands; sets up "unit panels" (i.e. panels with rows of icons) for the currently selected
* unit.
*
* @param guiName Short identifier string of this panel; see constants defined at the top of this file.
* @param usedPanels Output object; usedPanels[guiName] will be set to 1 to indicate that this panel was used during this
* run of updateUnitCommands and should not be hidden. TODO: why is this done this way instead of having
* updateUnitCommands keep track of this?
* @param unitEntState Entity state of the (first) selected unit.
* @param items Panel-specific data to construct the icons with.
* @param callback Callback function to argument to execute when an item's icon gets clicked. Takes a single 'item' argument.
*/
function setupUnitPanel(guiName, usedPanels, unitEntState, playerState, items, callback)
{
usedPanels[guiName] = 1;
var numberOfItems = items.length;
var selection = g_Selection.toList();
var garrisonGroups = new EntityGroups();
// Determine how many buttons there should be
switch (guiName)
{
case SELECTION:
if (numberOfItems > 16)
numberOfItems = 16;
break;
case QUEUE:
if (numberOfItems > 16)
numberOfItems = 16;
break;
case GARRISON:
if (numberOfItems > 12)
numberOfItems = 12;
break;
case STANCE:
if (numberOfItems > 5)
numberOfItems = 5;
break;
case FORMATION:
if (numberOfItems > 16)
numberOfItems = 16;
break;
case TRAINING:
if (numberOfItems > 24)
numberOfItems = 24;
break;
case RESEARCH:
if (numberOfItems > 8)
numberOfItems = 8;
break;
case CONSTRUCTION:
if (numberOfItems > 24)
numberOfItems = 24;
break;
case COMMAND:
if (numberOfItems > 6)
numberOfItems = 6;
break;
case GATE:
if(numberOfItems > 8)
numberOfItems = 8;
break;
case PACK:
if(numberOfItems > 8)
numberOfItems = 8;
break;
default:
break;
}
switch (guiName)
{
case GARRISON:
case COMMAND:
// Common code for garrison and 'unload all' button counts.
for (var i = 0; i < selection.length; ++i)
{
var state = GetEntityState(selection[i]);
if (state.garrisonHolder)
garrisonGroups.add(state.garrisonHolder.entities)
}
break;
default:
break;
}
var rowLength = 8;
if (guiName == SELECTION)
rowLength = 4;
else if (guiName == FORMATION || guiName == GARRISON || guiName == COMMAND)
rowLength = 4;
// Make buttons
var i;
for (i = 0; i < numberOfItems; i++)
{
var item = items[i];
// If a tech has been researched it leaves an empty slot
if (guiName == RESEARCH && !item)
{
getGUIObjectByName("unit"+guiName+"Button["+i+"]").hidden = true;
// We also remove the paired tech and the pair symbol
getGUIObjectByName("unit"+guiName+"Button["+(i+rowLength)+"]").hidden = true;
getGUIObjectByName("unit"+guiName+"Pair["+i+"]").hidden = true;
continue;
}
// Get the entity type and load the template for that type if necessary
var entType;
var template;
var entType1;
var template1;
switch (guiName)
{
case QUEUE:
// The queue can hold both technologies and units so we need to use the correct code for
// loading the templates
if (item.unitTemplate)
{
entType = item.unitTemplate;
template = GetTemplateData(entType);
}
else if (item.technologyTemplate)
{
entType = item.technologyTemplate;
template = GetTechnologyData(entType);
}
if (!template)
continue; // ignore attempts to use invalid templates (an error should have been
// reported already)
break;
case RESEARCH:
if (item.pair)
{
entType1 = item.top;
template1 = GetTechnologyData(entType1);
if (!template1)
continue; // ignore attempts to use invalid templates (an error should have been
// reported already)
entType = item.bottom;
}
else
{
entType = item;
}
template = GetTechnologyData(entType);
if (!template)
continue; // ignore attempts to use invalid templates (an error should have been
// reported already)
break;
case SELECTION:
case GARRISON:
case TRAINING:
case CONSTRUCTION:
entType = item;
template = GetTemplateData(entType);
if (!template)
continue; // ignore attempts to use invalid templates (an error should have been
// reported already)
break;
}
switch (guiName)
{
case SELECTION:
var name = getEntityNames(template);
var tooltip = name;
var count = g_Selection.groups.getCount(item);
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 1 ? count : "");
break;
case QUEUE:
var tooltip = getEntityNames(template);
if (item.neededSlots)
tooltip += "\n[color=\"red\"]Insufficient population capacity:\n[/color]"+getCostComponentDisplayName("population")+" "+item.neededSlots;
var progress = Math.round(item.progress*100) + "%";
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (item.count > 1 ? item.count : "");
if (i == 0)
{
getGUIObjectByName("queueProgress").caption = (item.progress ? progress : "");
var size = getGUIObjectByName("unit"+guiName+"ProgressSlider["+i+"]").size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(item.progress * (size.right - size.left));
getGUIObjectByName("unit"+guiName+"ProgressSlider["+i+"]").size = size;
}
break;
case GARRISON:
var name = getEntityNames(template);
var tooltip = "Unload " + name + "\nSingle-click to unload 1. Shift-click to unload all of this type.";
var count = garrisonGroups.getCount(item);
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 1 ? count : "");
break;
case GATE:
var tooltip = item.tooltip;
if (item.template)
{
var template = GetTemplateData(item.template);
var wallCount = g_Selection.toList().reduce(function (count, ent) {
var state = GetEntityState(ent);
if (hasClass(state, "LongWall") && !state.gate)
count++;
return count;
}, 0);
tooltip += "\n" + getEntityCostTooltip(template, wallCount);
var affordableMask = getGUIObjectByName("unitGateUnaffordable["+i+"]");
affordableMask.hidden = true;
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", multiplyEntityCosts(template, wallCount));
if (neededResources)
{
affordableMask.hidden = false;
tooltip += getNeededResourcesTooltip(neededResources);
}
}
break;
case PACK:
var tooltip = item.tooltip;
break;
case STANCE:
case FORMATION:
var tooltip = toTitleCase(item);
break;
case TRAINING:
var tooltip = getEntityNamesFormatted(template);
var key = Engine.ConfigDB_GetValue("user", "hotkey.session.queueunit." + (i + 1));
if (key)
tooltip = "[color=\"255 251 131\"][font=\"serif-bold-16\"][" + key + "][/font][/color] " + tooltip;
if (template.tooltip)
tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingBatchStatus(playerState, unitEntState.id, entType, selection);
if (Engine.HotkeyIsPressed("session.batchtrain"))
trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
tooltip += "\n" + getEntityCostTooltip(template, trainNum, unitEntState.id);
var [trainEntLimit, trainEntCount, canBeAddedCount, trainEntLimitChangers] =
getEntityLimitAndCount(playerState, entType);
tooltip += formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers);
tooltip += "[color=\"255 251 131\"]" + formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) + "[/color]";
break;
case RESEARCH:
var tooltip = getEntityNamesFormatted(template);
if (template.tooltip)
tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
tooltip += "\n" + getEntityCostTooltip(template);
if (item.pair)
{
var tooltip1 = getEntityNamesFormatted(template1);
if (template1.tooltip)
tooltip1 += "\n[font=\"serif-13\"]" + template1.tooltip + "[/font]";
tooltip1 += "\n" + getEntityCostTooltip(template1);
}
break;
case CONSTRUCTION:
var tooltip = getEntityNamesFormatted(template);
if (template.tooltip)
tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
tooltip += "\n" + getEntityCostTooltip(template);
tooltip += getPopulationBonusTooltip(template);
var [entLimit, entCount, canBeAddedCount, entLimitChangers] =
getEntityLimitAndCount(playerState, entType);
tooltip += formatLimitString(entLimit, entCount, entLimitChangers);
break;
case COMMAND:
// here, "item" is an object with properties .name (command name), .tooltip and .icon (relative to session/icons/single)
if (item.name == "unload-all")
{
var count = garrisonGroups.getTotalCount();
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 0 ? count : "");
}
else
{
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = "";
}
tooltip = (item.tooltip ? item.tooltip : toTitleCase(item.name));
break;
default:
break;
}
// Button
var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]");
var button1 = getGUIObjectByName("unit"+guiName+"Button["+(i+rowLength)+"]");
var affordableMask = getGUIObjectByName("unit"+guiName+"Unaffordable["+i+"]");
var affordableMask1 = getGUIObjectByName("unit"+guiName+"Unaffordable["+(i+rowLength)+"]");
var icon = getGUIObjectByName("unit"+guiName+"Icon["+i+"]");
var guiSelection = getGUIObjectByName("unit"+guiName+"Selection["+i+"]");
var pair = getGUIObjectByName("unit"+guiName+"Pair["+i+"]");
button.hidden = false;
button.tooltip = tooltip;
// Button Function (need nested functions to get the closure right)
// Items can have a callback element that overrides the normal caller-supplied callback function.
button.onpress = (function(e){ return function() { e.callback ? e.callback(e) : callback(e) } })(item);
if(guiName == SELECTION)
{
button.onpressright = (function(e){return function() {callback(e, true) } })(item);
button.onpress = (function(e){ return function() {callback(e, false) } })(item);
}
if (guiName == RESEARCH)
{
if (item.pair)
{
button.onpress = (function(e){ return function() { callback(e) } })(item.bottom);
var icon1 = getGUIObjectByName("unit"+guiName+"Icon["+(i+rowLength)+"]");
button1.hidden = false;
button1.tooltip = tooltip1;
button1.onpress = (function(e){ return function() { callback(e) } })(item.top);
// when we hover over a pair, the other one gets a red cross over it to show it won't be available any more.
var unchosenIcon = getGUIObjectByName("unit"+guiName+"UnchosenIcon["+i+"]");
var unchosenIcon1 = getGUIObjectByName("unit"+guiName+"UnchosenIcon["+(i+rowLength)+"]");
button1.onmouseenter = (function(e){ return function() { setOverlay(e, true) } })(unchosenIcon);
button1.onmouseleave = (function(e){ return function() { setOverlay(e, false) } })(unchosenIcon);
button.onmouseenter = (function(e){ return function() { setOverlay(e, true) } })(unchosenIcon1);
button.onmouseleave = (function(e){ return function() { setOverlay(e, false) } })(unchosenIcon1);
pair.hidden = false;
}
else
{
// Hide the overlay.
var unchosenIcon = getGUIObjectByName("unit"+guiName+"UnchosenIcon["+i+"]");
unchosenIcon.hidden = true;
}
}
// Get icon image
if (guiName == FORMATION)
{
var formationOk = canMoveSelectionIntoFormation(item);
var grayscale = "";
button.enabled = formationOk;
if (!formationOk)
{
grayscale = "grayscale:";
// Display a meaningful tooltip why the formation is disabled
var requirements = Engine.GuiInterfaceCall("GetFormationRequirements", {
"formationName": item
});
button.tooltip += " (disabled)";
if (requirements.count > 1)
button.tooltip += "\n" + requirements.count + " units required";
if (requirements.classesRequired)
{
button.tooltip += "\nOnly units of type";
for each (var classRequired in requirements.classesRequired)
{
button.tooltip += " " + classRequired;
}
button.tooltip += " allowed.";
}
}
var formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": g_Selection.toList(),
"formationName": item
});
guiSelection.hidden = !formationSelected;
icon.sprite = "stretched:"+grayscale+"session/icons/formations/"+item.replace(/\s+/,'').toLowerCase()+".png";
}
else if (guiName == STANCE)
{
var stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": g_Selection.toList(),
"stance": item
});
guiSelection.hidden = !stanceSelected;
icon.sprite = "stretched:session/icons/stances/"+item+".png";
}
else if (guiName == COMMAND)
{
icon.sprite = "stretched:session/icons/" + item.icon;
}
else if (guiName == GATE)
{
var gateIcon;
// If already a gate, show locking actions
if (item.gate)
{
gateIcon = "icons/lock_" + GATE_ACTIONS[item.locked ? 0 : 1].toLowerCase() + "ed.png";
guiSelection.hidden = item.gate.locked === undefined ? false : item.gate.locked != item.locked;
}
// otherwise show gate upgrade icon
else
{
template = GetTemplateData(item.template);
gateIcon = template.icon ? "portraits/" + template.icon : "icons/gate_closed.png";
guiSelection.hidden = true;
}
icon.sprite = "stretched:session/" + gateIcon;
}
else if (guiName == PACK)
{
if (item.packing)
{
icon.sprite = "stretched:session/icons/cancel.png";
}
else
{
if (item.packed)
icon.sprite = "stretched:session/icons/unpack.png";
else
icon.sprite = "stretched:session/icons/pack.png";
}
}
else if (template.icon)
{
var grayscale = "";
button.enabled = true;
if (affordableMask)
affordableMask.hidden = true; // actually used for the red "lack of resource" overlay, and darkening if unavailable. Sort of a hack.
// In case this is an icon that would require tech checking, make sure we have the requirements.
if (guiName != SELECTION && guiName != GARRISON && guiName != QUEUE && template.requiredTechnology && !Engine.GuiInterfaceCall("IsTechnologyResearched", template.requiredTechnology))
{
button.enabled = false;
var techName = getEntityNames(GetTechnologyData(template.requiredTechnology));
button.tooltip += "\nRequires " + techName;
grayscale = "grayscale:";
affordableMask.hidden = false;
affordableMask.sprite = "colour: 0 0 0 127";
}
if (guiName == RESEARCH && !Engine.GuiInterfaceCall("CheckTechnologyRequirements", entType))
{
button.enabled = false;
button.tooltip += "\n" + GetTechnologyData(entType).requirementsTooltip;
grayscale = "grayscale:";
affordableMask.hidden = false;
affordableMask.sprite = "colour: 0 0 0 127";
}
if ((guiName == CONSTRUCTION || guiName == TRAINING) && canBeAddedCount == 0)
{
grayscale = "grayscale:";
affordableMask.hidden = false;
affordableMask.sprite = "colour: 0 0 0 127";
}
if (guiName == GARRISON)
{
var ents = garrisonGroups.getEntsByName(item);
var entplayer = GetEntityState(ents[0]).player;
button.sprite = "colour: " + g_Players[entplayer].color.r + " " + g_Players[entplayer].color.g + " " + g_Players[entplayer].color.b;
var player = Engine.GetPlayerID();
if(player != unitEntState.player && !g_DevSettings.controlAll)
{
if (entplayer != player)
{
button.enabled = false;
grayscale = "grayscale:";
}
}
}
icon.sprite = "stretched:" + grayscale + "session/portraits/" + template.icon;
if (guiName == RESEARCH)
{
// Check resource requirements
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", template.cost);
if (neededResources)
{
if (button.enabled !== false)
{
button.enabled = false;
affordableMask.hidden = false;
var totalCost = 0;
for each (var resource in neededResources)
totalCost += resource;
var alpha = 50 + totalCost/10;
alpha = alpha > 125 ? 125 : alpha;
affordableMask.sprite = "colour: 255 0 0 " + (alpha);
}
button.tooltip += getNeededResourcesTooltip(neededResources);
}
if (item.pair)
{
grayscale = "";
button1.enabled = true;
affordableMask1.hidden = true;
if (!Engine.GuiInterfaceCall("CheckTechnologyRequirements", entType1))
{
button1.enabled = false;
button1.tooltip += "\n" + GetTechnologyData(entType1).requirementsTooltip;
grayscale = "grayscale:";
affordableMask1.hidden = false;
affordableMask1.sprite = "colour: 0 0 0 127";
}
icon1.sprite = "stretched:" + grayscale + "session/portraits/" +template1.icon;
// Check resource requirements for second button
neededResources = Engine.GuiInterfaceCall("GetNeededResources", template1.cost);
if (neededResources)
{
if (button1.enabled !== false)
{
button1.enabled = false;
affordableMask1.hidden = false;
var totalCost = 0;
for each (var resource in neededResources)
totalCost += resource;
var alpha = 50 + totalCost/10;
alpha = alpha > 125 ? 125 : alpha;
affordableMask1.sprite = "colour: 255 0 0 " + (alpha);
}
button1.tooltip += getNeededResourcesTooltip(neededResources);
}
}
else
{
pair.hidden = true;
button1.hidden = true;
affordableMask1.hidden = true;
}
}
else if (guiName == CONSTRUCTION || guiName == TRAINING)
{
var totalCosts = {};
var trainNum = 1;
var button_disableable = true;
if (guiName == TRAINING)
{
if (Engine.HotkeyIsPressed("session.batchtrain"))
{
var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch, batchTrainingCount] =
getTrainingBatchStatus(playerState, unitEntState.id, entType, selection);
trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
button_disableable = !Engine.HotkeyIsPressed("selection.remove");
}
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (batchTrainingCount > 0) ? batchTrainingCount : "";
}
// Walls have no cost defined.
if (template.cost !== undefined)
totalCosts = multiplyEntityCosts(template, trainNum);
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", totalCosts);
if (neededResources)
{
if (button.enabled !== false)
{
button.enabled = (button_disableable ? false : true);
// Don't display the red overlay if we can't even train/build it
if (canBeAddedCount != 0)
{
affordableMask.hidden = false;
var totalCost = 0;
for each (var resource in neededResources)
totalCost += resource;
var alpha = 50 + totalCost/10;
alpha = alpha > 125 ? 125 : alpha;
affordableMask.sprite = "colour: 255 0 0 " + (alpha);
}
}
button.tooltip += getNeededResourcesTooltip(neededResources);
}
}
}
else
{
// TODO: we should require all entities to have icons, so this case never occurs
icon.sprite = "bkFillBlack";
}
}
// Position the visible buttons (TODO: if there's lots, maybe they should be squeezed together to fit)
var numButtons = i;
var numRows = Math.ceil(numButtons / rowLength);
var buttonSideLength = getGUIObjectByName("unit"+guiName+"Button[0]").size.bottom;
// We sort pairs upside down, so get the size from the topmost button.
if (guiName == RESEARCH)
buttonSideLength = getGUIObjectByName("unit"+guiName+"Button["+(rowLength*numRows)+"]").size.bottom;
var buttonSpacer = buttonSideLength+1;
// Layout buttons
if (guiName == COMMAND)
{
layoutButtonRowCentered(0, guiName, 0, numButtons, COMMANDS_PANEL_WIDTH);
}
else if (guiName == RESEARCH)
{
// We support pairs so we need to add a row
numRows++;
// Layout rows from bottom to top
for (var i = 0, j = numRows; i < numRows; i++, j--)
{
layoutButtonRow(i, guiName, buttonSideLength, buttonSpacer, rowLength*(j-1), rowLength*j);
}
}
else
{
for (var i = 0; i < numRows; i++)
layoutButtonRow(i, guiName, buttonSideLength, buttonSpacer, rowLength*i, rowLength*(i+1) );
}
// Layout pair icons
if (guiName == RESEARCH)
{
var pairSize = getGUIObjectByName("unit"+guiName+"Pair[0]").size;
var pairSideWidth = pairSize.right;
var pairSideHeight = pairSize.bottom;
var pairSpacerHeight = pairSideHeight + 1;
var pairSpacerWidth = pairSideWidth + 1;
layoutRow("Pair", 0, guiName, pairSideWidth, pairSpacerWidth, pairSideHeight, pairSpacerHeight, 0, rowLength);
}
// Resize Queue panel if needed
if (guiName == QUEUE) // or garrison
{
var panel = getGUIObjectByName("unitQueuePanel");
var size = panel.size;
size.top = (UNIT_PANEL_BASE - ((numRows-1)*UNIT_PANEL_HEIGHT));
panel.size = size;
}
// Hide any buttons we're no longer using
for (var i = numButtons; i < g_unitPanelButtons[guiName]; ++i)
getGUIObjectByName("unit"+guiName+"Button["+i+"]").hidden = true;
// Hide unused pair buttons and symbols
if (guiName == RESEARCH)
{
for (var i = numButtons; i < g_unitPanelButtons[guiName]; ++i)
{
getGUIObjectByName("unit"+guiName+"Button["+(i+rowLength)+"]").hidden = true;
getGUIObjectByName("unit"+guiName+"Pair["+i+"]").hidden = true;
}
}
g_unitPanelButtons[guiName] = numButtons;
}
// Sets up "unit trading panel" - special case for setupUnitPanel
function setupUnitTradingPanel(usedPanels, unitEntState, selection)
{
usedPanels[TRADING] = 1;
+ var requiredGoods = unitEntState.trader.requiredGoods;
for (var i = 0; i < TRADING_RESOURCES.length; i++)
{
var resource = TRADING_RESOURCES[i];
var button = getGUIObjectByName("unitTradingButton["+i+"]");
button.size = (i * 46) + " 0 " + ((i + 1) * 46) + " 46";
- var selectTradingPreferredGoodsData = { "entities": selection, "preferredGoods": resource };
- button.onpress = (function(e){ return function() { selectTradingPreferredGoods(e); } })(selectTradingPreferredGoodsData);
+ if (resource == requiredGoods)
+ var selectRequiredGoodsData = { "entities": selection, "requiredGoods": undefined };
+ else
+ var selectRequiredGoodsData = { "entities": selection, "requiredGoods": resource };
+ button.onpress = (function(e){ return function() { selectRequiredGoods(e); } })(selectRequiredGoodsData);
button.enabled = true;
- button.tooltip = "Set " + resource + " as trading goods";
+ button.tooltip = "Set/unset " + resource + " as forced trading goods.";
var icon = getGUIObjectByName("unitTradingIcon["+i+"]");
- var preferredGoods = unitEntState.trader.preferredGoods;
var selected = getGUIObjectByName("unitTradingSelection["+i+"]");
- selected.hidden = !(resource == preferredGoods);
- var grayscale = (resource != preferredGoods) ? "grayscale:" : "";
+ selected.hidden = !(resource == requiredGoods);
+ var grayscale = (resource != requiredGoods) ? "grayscale:" : "";
icon.sprite = "stretched:"+grayscale+"session/icons/resources/" + resource + ".png";
}
}
// Sets up "unit barter panel" - special case for setupUnitPanel
function setupUnitBarterPanel(unitEntState, playerState)
{
// Amount of player's resource to exchange
var amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL;
if (Engine.HotkeyIsPressed("session.massbarter"))
amountToSell *= BARTER_BUNCH_MULTIPLIER;
// One pass for each resource
for (var i = 0; i < BARTER_RESOURCES.length; i++)
{
var resource = BARTER_RESOURCES[i];
// One pass for 'sell' row and another for 'buy'
for (var j = 0; j < 2; j++)
{
var action = BARTER_ACTIONS[j];
if (j == 0)
{
// Display the selection overlay
var selection = getGUIObjectByName("unitBarter" + action + "Selection["+i+"]");
selection.hidden = !(i == g_barterSell);
}
// We gray out the not selected icons in 'sell' row
var grayscale = (j == 0 && i != g_barterSell) ? "grayscale:" : "";
var icon = getGUIObjectByName("unitBarter" + action + "Icon["+i+"]");
var button = getGUIObjectByName("unitBarter" + action + "Button["+i+"]");
button.size = (i * 46) + " 0 " + ((i + 1) * 46) + " 46";
var amountToBuy;
// We don't display a button in 'buy' row if the same resource is selected in 'sell' row
if (j == 1 && i == g_barterSell)
{
button.hidden = true;
}
else
{
button.hidden = false;
button.tooltip = action + " " + resource;
icon.sprite = "stretched:"+grayscale+"session/icons/resources/" + resource + ".png";
var sellPrice = unitEntState.barterMarket.prices["sell"][BARTER_RESOURCES[g_barterSell]];
var buyPrice = unitEntState.barterMarket.prices["buy"][resource];
amountToBuy = "+" + Math.round(sellPrice / buyPrice * amountToSell);
}
var amount;
if (j == 0)
{
button.onpress = (function(i){ return function() { g_barterSell = i; } })(i);
if (i == g_barterSell)
{
amount = "-" + amountToSell;
var neededRes = {};
neededRes[resource] = amountToSell;
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", neededRes);
var hidden = neededResources ? false : true;
for (var ii = 0; ii < BARTER_RESOURCES.length; ii++)
{
var affordableMask = getGUIObjectByName("unitBarterBuyUnaffordable["+ii+"]");
affordableMask.hidden = hidden;
}
}
else
amount = "";
}
else
{
var exchangeResourcesParameters = { "sell": BARTER_RESOURCES[g_barterSell], "buy": BARTER_RESOURCES[i], "amount": amountToSell };
button.onpress = (function(exchangeResourcesParameters){ return function() { exchangeResources(exchangeResourcesParameters); } })(exchangeResourcesParameters);
amount = amountToBuy;
}
getGUIObjectByName("unitBarter" + action + "Amount["+i+"]").caption = amount;
}
}
}
/**
* Updates the right hand side "Unit Commands" panel. Runs in the main session loop via updateSelectionDetails().
* Delegates to setupUnitPanel to set up individual subpanels, appropriately activated depending on the selected
* unit's state.
*
* @param entState Entity state of the (first) selected unit.
* @param supplementalDetailsPanel Reference to the "supplementalSelectionDetails" GUI Object
* @param commandsPanel Reference to the "commandsPanel" GUI Object
* @param selection Array of currently selected entity IDs.
*/
function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection)
{
// Panels that are active
var usedPanels = {};
// If the selection is friendly units, add the command panels
var player = Engine.GetPlayerID();
// Get player state to check some constraints
// e.g. presence of a hero or build limits
var simState = GetSimState();
var playerState = simState.players[player];
if (entState.player == player || g_DevSettings.controlAll)
{
if (selection.length > 1)
setupUnitPanel(SELECTION, usedPanels, entState, playerState, g_Selection.groups.getTemplateNames(),
function (entType, rightPressed) { changePrimarySelectionGroup(entType, rightPressed); } );
var commands = getEntityCommandsList(entState);
if (commands.length)
setupUnitPanel(COMMAND, usedPanels, entState, playerState, commands,
function (item) { performCommand(entState.id, item.name); } );
if (entState.garrisonHolder)
{
var groups = new EntityGroups();
for (var i in selection)
{
var state = GetEntityState(selection[i]);
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities)
}
setupUnitPanel(GARRISON, usedPanels, entState, playerState, groups.getTemplateNames(),
function (item) { unloadTemplate(item); } );
}
var formations = Engine.GuiInterfaceCall("GetAvailableFormations");
if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && !entState.garrisonHolder && formations.length)
{
setupUnitPanel(FORMATION, usedPanels, entState, playerState, formations,
function (item) { performFormation(entState.id, item); } );
}
// TODO: probably should load the stance list from a data file,
// and/or vary depending on what units are selected
var stances = ["violent", "aggressive", "passive", "defensive", "standground"];
if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && stances.length)
{
setupUnitPanel(STANCE, usedPanels, entState, playerState, stances,
function (item) { performStance(entState.id, item); } );
}
getGUIObjectByName("unitBarterPanel").hidden = !entState.barterMarket;
if (entState.barterMarket)
{
usedPanels["Barter"] = 1;
setupUnitBarterPanel(entState, playerState);
}
var buildableEnts = getAllBuildableEntitiesFromSelection();
var trainableEnts = getAllTrainableEntitiesFromSelection();
// Whether the GUI's right panel has been filled.
var rightUsed = true;
// The first selected entity's type has priority.
if (entState.buildEntities)
setupUnitPanel(CONSTRUCTION, usedPanels, entState, playerState, buildableEnts,
function (trainEntType) { startBuildingPlacement(trainEntType, playerState); } );
else if (entState.production && entState.production.entities)
setupUnitPanel(TRAINING, usedPanels, entState, playerState, trainableEnts,
function (trainEntType) { addTrainingToQueue(selection, trainEntType, playerState); } );
- else if (entState.trader)
- setupUnitTradingPanel(usedPanels, entState, selection);
+// else if (entState.trader)
+// setupUnitTradingPanel(usedPanels, entState, selection);
else if (!entState.foundation && entState.gate || hasClass(entState, "LongWall"))
{
// Allow long wall pieces to be converted to gates
var longWallTypes = {};
var walls = [];
var gates = [];
for (var i in selection)
{
var state = GetEntityState(selection[i]);
if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template])
{
var gateTemplate = getWallGateTemplate(state.id);
if (gateTemplate)
{
var wallName = GetTemplateData(state.template).name.generic;
var gateName = GetTemplateData(gateTemplate).name.generic;
walls.push({
"tooltip": "Convert " + wallName + " to " + gateName,
"template": gateTemplate,
"callback": function (item) { transformWallToGate(item.template); }
});
}
// We only need one entity per type.
longWallTypes[state.template] = true;
}
else if (state.gate && !gates.length)
for (var j = 0; j < GATE_ACTIONS.length; ++j)
gates.push({
"gate": state.gate,
"tooltip": GATE_ACTIONS[j] + " gate",
"locked": j == 0,
"callback": function (item) { lockGate(item.locked); }
});
// Show both 'locked' and 'unlocked' as active if the selected gates have both lock states.
else if (state.gate && state.gate.locked != gates[0].gate.locked)
for (var j = 0; j < gates.length; ++j)
delete gates[j].gate.locked;
}
// Place wall conversion options after gate lock/unlock icons.
var items = gates.concat(walls);
if (items.length)
setupUnitPanel(GATE, usedPanels, entState, playerState, items);
else
rightUsed = false;
}
else if (entState.pack)
{
var items = [];
var packButton = false;
var unpackButton = false;
var packCancelButton = false;
var unpackCancelButton = false;
for (var i in selection)
{
// Find un/packable entities
var state = GetEntityState(selection[i]);
if (state.pack)
{
if (state.pack.progress == 0)
{
if (!state.pack.packed)
packButton = true;
else if (state.pack.packed)
unpackButton = true;
}
else
{
// Already un/packing - show cancel button
if (!state.pack.packed)
packCancelButton = true;
else if (state.pack.packed)
unpackCancelButton = true;
}
}
}
if (packButton)
items.push({ "packing": false, "packed": false, "tooltip": "Pack", "callback": function() { packUnit(true); } });
if (unpackButton)
items.push({ "packing": false, "packed": true, "tooltip": "Unpack", "callback": function() { packUnit(false); } });
if (packCancelButton)
items.push({ "packing": true, "packed": false, "tooltip": "Cancel packing", "callback": function() { cancelPackUnit(true); } });
if (unpackCancelButton)
items.push({ "packing": true, "packed": true, "tooltip": "Cancel unpacking", "callback": function() { cancelPackUnit(false); } });
if (items.length)
setupUnitPanel(PACK, usedPanels, entState, playerState, items);
else
rightUsed = false;
}
else
rightUsed = false;
if (!rightUsed)
{
// The right pane is empty. Fill the pane with a sane type.
// Prefer buildables for units and trainables for structures.
if (buildableEnts.length && (hasClass(entState, "Unit") || !trainableEnts.length))
setupUnitPanel(CONSTRUCTION, usedPanels, entState, playerState, buildableEnts,
function (trainEntType) { startBuildingPlacement(trainEntType, playerState); });
else if (trainableEnts.length)
setupUnitPanel(TRAINING, usedPanels, entState, playerState, trainableEnts,
function (trainEntType) { addTrainingToQueue(selection, trainEntType, playerState); } );
}
// Show technologies if the active panel has at most one row of icons.
if (entState.production && entState.production.technologies.length)
{
var activepane = usedPanels[CONSTRUCTION] ? buildableEnts.length : trainableEnts.length;
if (selection.length == 1 || activepane <= 8)
setupUnitPanel(RESEARCH, usedPanels, entState, playerState, entState.production.technologies,
function (researchType) { addResearchToQueue(entState.id, researchType); } );
}
if (entState.production && entState.production.queue.length)
setupUnitPanel(QUEUE, usedPanels, entState, playerState, entState.production.queue,
function (item) { removeFromProductionQueue(entState.id, item.id); } );
supplementalDetailsPanel.hidden = false;
commandsPanel.hidden = false;
}
else if (playerState.isMutualAlly[entState.player]) // owned by allied player
{
if (entState.garrisonHolder)
{
var groups = new EntityGroups();
for (var i in selection)
{
var state = GetEntityState(selection[i]);
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities)
}
setupUnitPanel(GARRISON, usedPanels, entState, playerState, groups.getTemplateNames(),
function (item) { unloadTemplate(item); } );
supplementalDetailsPanel.hidden = false;
}
else
{
supplementalDetailsPanel.hidden = true;
}
commandsPanel.hidden = true;
}
else // owned by another player
{
supplementalDetailsPanel.hidden = true;
commandsPanel.hidden = true;
}
// Hides / unhides Unit Panels (panels should be grouped by type, not by order, but we will leave that for another time)
var offset = 0;
for each (var panelName in g_unitPanels)
{
var panel = getGUIObjectByName("unit" + panelName + "Panel");
if (usedPanels[panelName])
panel.hidden = false;
else
panel.hidden = true;
}
}
// Force hide commands panels
function hideUnitCommands()
{
for each (var panelName in g_unitPanels)
getGUIObjectByName("unit" + panelName + "Panel").hidden = true;
}
// Get all of the available entities which can be trained by the selected entities
function getAllTrainableEntities(selection)
{
var trainableEnts = [];
var state;
// Get all buildable and trainable entities
for (var i in selection)
{
if ((state = GetEntityState(selection[i])) && state.production && state.production.entities.length)
trainableEnts = trainableEnts.concat(state.production.entities);
}
// Remove duplicates
removeDupes(trainableEnts);
return trainableEnts;
}
function getAllTrainableEntitiesFromSelection()
{
if (!g_allTrainableEntities)
g_allTrainableEntities = getAllTrainableEntities(g_Selection.toList());
return g_allTrainableEntities;
}
// Get all of the available entities which can be built by the selected entities
function getAllBuildableEntities(selection)
{
var buildableEnts = [];
var state;
// Get all buildable entities
for (var i in selection)
{
if ((state = GetEntityState(selection[i])) && state.buildEntities && state.buildEntities.length)
buildableEnts = buildableEnts.concat(state.buildEntities);
}
// Remove duplicates
removeDupes(buildableEnts);
return buildableEnts;
}
function getAllBuildableEntitiesFromSelection()
{
if (!g_allBuildableEntities)
g_allBuildableEntities = getAllBuildableEntities(g_Selection.toList());
return g_allBuildableEntities;
}
// Check if the selection can move into formation, and cache the result
function canMoveSelectionIntoFormation(formationName)
{
if (!(formationName in g_canMoveIntoFormation))
{
g_canMoveIntoFormation[formationName] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
"formationName": formationName
});
}
return g_canMoveIntoFormation[formationName];
}
Index: ps/trunk/binaries/data/mods/public/gui/session/utility_functions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/utility_functions.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/gui/session/utility_functions.js (revision 14417)
@@ -1,632 +1,641 @@
const GEOLOGY = "geology";
const FLORA = "flora";
const FAUNA = "fauna";
const SPECIAL = "special";
const COST_DISPLAY_NAMES = {
"food": "[icon=\"iconFood\"]",
"wood": "[icon=\"iconWood\"]",
"stone": "[icon=\"iconStone\"]",
"metal": "[icon=\"iconMetal\"]",
"population": "[icon=\"iconPopulation\"]",
"time": "[icon=\"iconTime\"]"
};
//-------------------------------- -------------------------------- --------------------------------
// Utility functions
//-------------------------------- -------------------------------- --------------------------------
function toTitleCase(word)
{
if (word.length > 0)
{
var titleCased = word.substring(0, 1).toUpperCase();
if (word.length > 1)
{
titleCased += word.substring(1).toLowerCase();
}
return titleCased;
}
return word;
}
// Get the basic player data
function getPlayerData(playerAssignments)
{
var players = [];
var simState = GetSimState();
if (!simState)
return players;
for (var i = 0; i < simState.players.length; i++)
{
var playerState = simState.players[i];
var name = playerState.name;
var civ = playerState.civ;
var color = {
"r": playerState.colour.r*255,
"g": playerState.colour.g*255,
"b": playerState.colour.b*255,
"a": playerState.colour.a*255
};
var player = {
"name": name,
"civ": civ,
"color": color,
"team": playerState.team,
"teamsLocked": playerState.teamsLocked,
"cheatsEnabled": playerState.cheatsEnabled,
"state": playerState.state,
"isAlly": playerState.isAlly,
"isMutualAlly": playerState.isMutualAlly,
"isNeutral": playerState.isNeutral,
"isEnemy": playerState.isEnemy,
"guid": undefined, // network guid for players controlled by hosts
"disconnected": false // flag for host-controlled players who have left the game
};
players.push(player);
}
// Overwrite default player names with multiplayer names
if (playerAssignments)
{
for (var playerGuid in playerAssignments)
{
var playerAssignment = playerAssignments[playerGuid];
if (players[playerAssignment.player])
{
players[playerAssignment.player].guid = playerGuid;
players[playerAssignment.player].name = playerAssignment.name;
}
}
}
return players;
}
function findGuidForPlayerID(playerAssignments, player)
{
for (var playerGuid in playerAssignments)
{
var playerAssignment = playerAssignments[playerGuid];
if (playerAssignment.player == player)
return playerGuid;
}
return undefined;
}
// Update player data when a host has connected
function updatePlayerDataAdd(players, hostGuid, playerAssignment)
{
if (players[playerAssignment.player])
{
players[playerAssignment.player].guid = hostGuid;
players[playerAssignment.player].name = playerAssignment.name;
players[playerAssignment.player].offline = false;
}
}
// Update player data when a host has disconnected
function updatePlayerDataRemove(players, hostGuid)
{
for each (var player in players)
if (player.guid == hostGuid)
player.offline = true;
}
function hasClass(entState, className)
{
if (entState.identity)
{
var classes = entState.identity.classes;
if (classes && classes.length)
return (classes.indexOf(className) != -1);
}
return false;
}
// For the unit details panel
function damageValues(dmg)
{
if (dmg)
{
var dmgArray = [];
dmg.hack? dmgArray.push(dmg.hack) : dmgArray.push(0);
dmg.pierce? dmgArray.push(dmg.pierce) : dmgArray.push(0);
dmg.crush? dmgArray.push(dmg.crush) : dmgArray.push(0);
return dmgArray;
}
else
{
return [0, 0, 0];
}
}
// For the unit details panel
function damageTypeDetails(dmg)
{
if (dmg)
{
var dmgArray = [];
if (dmg.hack) dmgArray.push(dmg.hack + "[font=\"sans-10\"][color=\"orange\"] Hack[/color][/font]");
if (dmg.pierce) dmgArray.push(dmg.pierce + "[font=\"sans-10\"][color=\"orange\"] Pierce[/color][/font]");
if (dmg.crush) dmgArray.push(dmg.crush + "[font=\"sans-10\"][color=\"orange\"] Crush[/color][/font]");
return dmgArray.join(", ");
}
else
{
return "[font=\"serif-12\"](None)[/font]";
}
}
// Converts an armor level into the actual reduction percentage
function armorLevelToPercentage(level)
{
return 100 - Math.round(Math.pow(0.9, level) * 100);
}
// Also for the unit details panel
function armorTypeDetails(dmg)
{
if (dmg)
{
var dmgArray = [];
if (dmg.hack)
{
dmgArray.push(dmg.hack + "[font=\"sans-10\"][color=\"orange\"] Hack[/color][/font] " +
" [font=\"sans-10\"](" + armorLevelToPercentage(dmg.hack) + "%)[/font]");
}
if (dmg.pierce)
{
dmgArray.push(dmg.pierce + "[font=\"sans-10\"][color=\"orange\"] Pierce[/color][/font] " +
" [font=\"sans-10\"](" + armorLevelToPercentage(dmg.pierce) + "%)[/font]");
}
if (dmg.crush)
{
dmgArray.push(dmg.crush + "[font=\"sans-10\"][color=\"orange\"] Crush[/color][/font] " +
" [font=\"sans-10\"](" + armorLevelToPercentage(dmg.crush) + "%)[/font]");
}
return dmgArray.join(", ");
}
else
{
return "[font=\"serif-12\"](None)[/font]";
}
}
// For the training tooltip
function damageTypesToText(dmg)
{
if (!dmg)
return "[font=\"serif-12\"](None)[/font]";
var hackLabel = "[font=\"serif-12\"] Hack[/font]";
var pierceLabel = "[font=\"serif-12\"] Pierce[/font]";
var crushLabel = "[font=\"serif-12\"] Crush[/font]";
var hackDamage = dmg.hack;
var pierceDamage = dmg.pierce;
var crushDamage = dmg.crush;
var dmgArray = [];
if (hackDamage) dmgArray.push(Math.round(hackDamage) + hackLabel);
if (pierceDamage) dmgArray.push(Math.round(pierceDamage) + pierceLabel);
if (crushDamage) dmgArray.push(Math.round(crushDamage) + crushLabel);
return dmgArray.join("[font=\"serif-12\"], [/font]");
}
// Also for the training tooltip
function armorTypesToText(dmg)
{
if (!dmg)
return "[font=\"serif-12\"](None)[/font]";
var hackDamage = dmg.hack;
var pierceDamage = dmg.pierce;
var crushDamage = dmg.crush;
var hackLabel = "[font=\"serif-12\"] Hack (" + armorLevelToPercentage(hackDamage) + "%)[/font]";
var pierceLabel = "[font=\"serif-12\"] Pierce (" + armorLevelToPercentage(pierceDamage) + "%)[/font]";
var crushLabel = "[font=\"serif-12\"] Crush (" + armorLevelToPercentage(crushDamage) + "%)[/font]";
var dmgArray = [];
if (hackDamage) dmgArray.push(hackDamage + hackLabel);
if (pierceDamage) dmgArray.push(pierceDamage + pierceLabel);
if (crushDamage) dmgArray.push(crushDamage + crushLabel);
return dmgArray.join("[font=\"serif-12\"], [/font]");
}
function getEntityCommandsList(entState)
{
var commands = [];
if (entState.garrisonHolder)
{
commands.push({
"name": "unload-all",
"tooltip": "Unload All",
"icon": "garrison-out.png"
});
}
commands.push({
"name": "delete",
"tooltip": "Delete",
"icon": "kill_small.png"
});
if (hasClass(entState, "Unit"))
{
commands.push({
"name": "stop",
"tooltip": "Stop",
"icon": "stop.png"
});
commands.push({
"name": "garrison",
"tooltip": "Garrison",
"icon": "garrison.png"
});
}
if (entState.buildEntities)
{
commands.push({
"name": "repair",
"tooltip": "Repair",
"icon": "repair.png"
});
}
if (entState.rallyPoint)
{
commands.push({
"name": "focus-rally",
"tooltip": "Focus on Rally Point",
"icon": "focus-rally.png"
});
}
if (entState.unitAI && entState.unitAI.hasWorkOrders)
{
commands.push({
"name": "back-to-work",
"tooltip": "Back to Work",
"icon": "production.png"
});
}
if (entState.unitAI && entState.unitAI.canGuard && !entState.unitAI.isGuarding)
{
commands.push({
"name": "add-guard",
"tooltip": "Guard",
"icon": "add-guard.png"
});
}
if (entState.unitAI && entState.unitAI.isGuarding)
{
commands.push({
"name": "remove-guard",
"tooltip": "Remove guard",
"icon": "remove-guard.png"
});
}
-
+
+ if (hasClass(entState, "Market"))
+ {
+ commands.push({
+ "name": "select-trading-goods",
+ "tooltip": "Select trading goods",
+ "icon": "economics.png"
+ });
+ }
+
if(entState.alertRaiser)
{
if(entState.alertRaiser.canIncreaseLevel)
commands.push({
"name": "increase-alert-level",
"tooltip": "Raise the alert ! Or raise it again to protect more units.",
"icon": "bell_level1.png"
});
if(entState.alertRaiser.hasRaisedAlert)
commands.push({
"name": "alert-end",
"tooltip": "End of alert.",
"icon": "bell_level0.png"
});
}
return commands;
}
/**
* Translates a cost component identifier as they are used internally (e.g. "population", "food", etc.) to proper
* display names.
*/
function getCostComponentDisplayName(costComponentName)
{
return COST_DISPLAY_NAMES[costComponentName];
}
/**
* Multiplies the costs for a template by a given batch size.
*/
function multiplyEntityCosts(template, trainNum)
{
var totalCosts = {};
for (var r in template.cost)
totalCosts[r] = Math.floor(template.cost[r] * trainNum);
return totalCosts;
}
/**
* Helper function for getEntityCostTooltip.
*/
function getEntityCostComponentsTooltipString(template, trainNum, entity)
{
if (!trainNum)
trainNum = 1;
var totalCosts = multiplyEntityCosts(template, trainNum);
totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", {"entity": entity, "batchSize": trainNum}) : 1));
var costs = [];
if (totalCosts.food) costs.push(getCostComponentDisplayName("food") + " " + totalCosts.food);
if (totalCosts.wood) costs.push(getCostComponentDisplayName("wood") + " " + totalCosts.wood);
if (totalCosts.metal) costs.push(getCostComponentDisplayName("metal") + " " + totalCosts.metal);
if (totalCosts.stone) costs.push(getCostComponentDisplayName("stone") + " " + totalCosts.stone);
if (totalCosts.population) costs.push(getCostComponentDisplayName("population") + " " + totalCosts.population);
if (totalCosts.time) costs.push(getCostComponentDisplayName("time") + " " + totalCosts.time);
return costs;
}
/**
* Returns an array of strings for a set of wall pieces. If the pieces share
* resource type requirements, output will be of the form '10 to 30 Stone',
* otherwise output will be, e.g. '10 Stone, 20 Stone, 30 Stone'.
*/
function getWallPieceTooltip(wallTypes)
{
var out = [];
var resourceCount = {};
// Initialize the acceptable types for '$x to $y $resource' mode.
for (var resource in wallTypes[0].cost)
if (wallTypes[0].cost[resource])
resourceCount[resource] = [wallTypes[0].cost[resource]];
var sameTypes = true;
for (var i = 1; i < wallTypes.length; ++i)
{
for (var resource in wallTypes[i].cost)
{
// Break out of the same-type mode if this wall requires
// resource types that the first didn't.
if (wallTypes[i].cost[resource] && !resourceCount[resource])
{
sameTypes = false;
break;
}
}
for (var resource in resourceCount)
{
if (wallTypes[i].cost[resource])
resourceCount[resource].push(wallTypes[i].cost[resource]);
else
{
sameTypes = false;
break;
}
}
}
if (sameTypes)
{
for (var resource in resourceCount)
{
var resourceMin = Math.min.apply(Math, resourceCount[resource]);
var resourceMax = Math.max.apply(Math, resourceCount[resource]);
out.push(getCostComponentDisplayName(resource) + " " + resourceMin + " to " + getCostComponentDisplayName(resource) + " " + resourceMax);
}
}
else
for (var i = 0; i < wallTypes.length; ++i)
out.push(getEntityCostComponentsTooltipString(wallTypes[i]).join(", "));
return out;
}
/**
* Returns the cost information to display in the specified entity's construction button tooltip.
*/
function getEntityCostTooltip(template, trainNum, entity)
{
var cost = "";
// Entities with a wallset component are proxies for initiating wall placement and as such do not have a cost of
// their own; the individual wall pieces within it do.
if (template.wallSet)
{
var templateLong = GetTemplateData(template.wallSet.templates.long);
var templateMedium = GetTemplateData(template.wallSet.templates.medium);
var templateShort = GetTemplateData(template.wallSet.templates.short);
var templateTower = GetTemplateData(template.wallSet.templates.tower);
var wallCosts = getWallPieceTooltip([templateShort, templateMedium, templateLong]);
var towerCosts = getEntityCostComponentsTooltipString(templateTower);
cost += "\n";
cost += " Walls: " + wallCosts.join(" ") + "\n";
cost += " Towers: " + towerCosts.join(" ");
}
else if (template.cost)
{
var costs = getEntityCostComponentsTooltipString(template, trainNum, entity);
cost += costs.join(" ");
}
else
{
cost = ""; // cleaner than duplicating the serif-bold-13 stuff
}
return cost;
}
/**
* Returns the population bonus information to display in the specified entity's construction button tooltip.
*/
function getPopulationBonusTooltip(template)
{
var popBonus = "";
if (template.cost && template.cost.populationBonus)
popBonus = "\n[font=\"serif-bold-13\"]Population Bonus:[/font] " + template.cost.populationBonus;
return popBonus;
}
/**
* Returns a message with the amount of each resource needed to create an entity.
*/
function getNeededResourcesTooltip(resources)
{
var formatted = [];
for (var resource in resources)
formatted.push("[font=\"serif-12\"]" + getCostComponentDisplayName(resource) + "[/font] " + resources[resource]);
return "\n\n[font=\"serif-bold-13\"][color=\"red\"]Insufficient resources:[/color][/font]\n" + formatted.join(" ");
}
function getEntitySpeed(template)
{
var speed = "";
if (template.speed)
{
speed += "[font=\"serif-bold-13\"]Speed:[/font] ";
var speeds = [];
if (template.speed.walk) speeds.push(template.speed.walk + " [font=\"serif-12\"]Walk[/font]");
if (template.speed.run) speeds.push(template.speed.run + " [font=\"serif-12\"]Run[/font]");
speed += speeds.join(", ");
}
return speed;
}
function getEntityAttack(template)
{
var attacks = [];
if (template.attack)
{
// Don't show slaughter attack
delete template.attack['Slaughter'];
for (var type in template.attack)
{
var attack = "[font=\"serif-bold-13\"]" + type + " Attack:[/font] " + damageTypesToText(template.attack[type]);
// Show max attack range if ranged attack, also convert to tiles (4m per tile)
if (type == "Ranged")
attack += ", [font=\"serif-bold-13\"]Range:[/font] "+Math.round(template.attack[type].maxRange/4);
attacks.push(attack);
}
}
return attacks.join("\n");
}
function getEntityName(template)
{
return template.name.specific || template.name.generic || "???";
}
function getEntityNames(template)
{
var names = [];
if (template.name.specific)
{
names.push(template.name.specific);
if (template.name.generic && names[0] != template.name.generic)
names.push("(" + template.name.generic + ")");
}
else if (template.name.generic)
names.push(template.name.generic);
return (names.length) ? names.join(" ") : "???";
}
function getEntityNamesFormatted(template)
{
var names = "";
var generic = template.name.generic;
var specific = template.name.specific;
if (specific)
{
// drop caps for specific name
names += '[font="serif-bold-16"]' + specific[0] + '[/font]' +
'[font="serif-bold-12"]' + specific.slice(1).toUpperCase() + '[/font]';
if (generic)
names += '[font="serif-bold-16"] (' + generic + ')[/font]';
}
else if (generic)
names = '[font="serif-bold-16"]' + generic + "[/font]";
else
names = "???";
return names;
}
function getEntityRankedName(entState)
{
var template = GetTemplateData(entState.template)
var rank = entState.identity.rank;
if (rank)
return rank + " " + template.name.specific;
else
return template.name.specific;
}
function getRankIconSprite(entState)
{
if ("Elite" == entState.identity.rank)
return "stretched:session/icons/rank3.png";
else if ("Advanced" == entState.identity.rank)
return "stretched:session/icons/rank2.png";
else if (entState.identity.classes && entState.identity.classes.length && -1 != entState.identity.classes.indexOf("CitizenSoldier"))
return "stretched:session/icons/rank1.png";
return "";
}
/**
* Returns a message with the details of the trade gain.
*/
function getTradingTooltip(gain)
{
var tooltip = gain.traderGain;
if (gain.market1Gain && gain.market1Owner == gain.traderOwner)
tooltip += "+" + gain.market1Gain;
if (gain.market2Gain && gain.market2Owner == gain.traderOwner)
tooltip += "+" + gain.market2Gain;
tooltip += " (you)";
if (gain.market1Gain && gain.market1Owner != gain.traderOwner)
tooltip += ", " + gain.market1Gain + " (player " + gain.market1Owner + ")";
if (gain.market2Gain && gain.market2Owner != gain.traderOwner)
tooltip += ", " + gain.market2Gain + " (player " + gain.market2Owner + ")";
return tooltip;
}
/**
* Returns the entity itself except when garrisoned where it returns its garrisonHolder
*/
function getEntityOrHolder(ent)
{
var entState = GetEntityState(ent);
if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length > 0 &&
entState.unitAI.orders[0].type == "Garrison")
return entState.unitAI.orders[0].data.target;
return ent;
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 14417)
@@ -1,1876 +1,1921 @@
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.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(),
"entityLimitChangers": cmpPlayerEntityLimits.GetLimitChangers(),
"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 = [];
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
var ret = {
"id": ent,
"template": template
};
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 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()
+ "requiredGoods": cmpTrader.GetRequiredGoods()
};
}
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
{
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage(),
"numBuilders": cmpFoundation.GetNumBuilders()
};
}
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
{
ret.player = cmpOwnership.GetOwner();
}
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
ret.rallyPoint = {'position': cmpRallyPoint.GetPositions()[0]}; // undefined or {x,z} object
}
var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClassesList(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
}
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
};
// Add some information needed for ungarrisoning
if (cmpUnitAI.isGarrisoned && ret.player !== undefined)
ret.template = "p" + ret.player + "&" + ret.template;
}
var cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
{
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
}
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
{
ret.gate = {
"locked": cmpGate.IsLocked(),
};
}
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if(cmpAlertRaiser)
{
ret.alertRaiser = {
"level": cmpAlertRaiser.GetLevel(),
"canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
"hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
};
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
return ret;
};
/**
* Get additionnal entity info, rarely used in the gui
*/
GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
{
var ret = {};
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
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;
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the rage in front of it, no spread. So angle = 0
ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), 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 cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
{
ret.obstruction = {
"controlGroup": cmpObstruction.GetControlGroup(),
"controlGroup2": cmpObstruction.GetControlGroup2(),
};
}
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 cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
{
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
}
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterMarket = { "prices": cmpBarter.GetPrices() };
}
var cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
{
ret.healer = {
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
}
return ret;
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var rot = {x:0, y:0, z:0};
var pos = {x:cmd.x,z:cmd.z};
pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z);
var elevationBonus = cmd.elevationBonus || 0;
var range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, extendedName)
{
var name = extendedName;
// Special case for garrisoned units which have a extended template
if (extendedName.indexOf("&") != -1)
name = extendedName.slice(extendedName.indexOf("&")+1);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(name);
if (!template)
return null;
var ret = {};
if (template.Armour)
{
ret.armour = {
"hack": ApplyValueModificationsToTemplate("Armour/Hack", +template.Armour.Hack, player, template),
"pierce": ApplyValueModificationsToTemplate("Armour/Pierce", +template.Armour.Hack, player, template),
"crush": ApplyValueModificationsToTemplate("Armour/Crush", +template.Armour.Hack, player, template),
};
}
if (template.Attack)
{
ret.attack = {};
for (var type in template.Attack)
{
ret.attack[type] = {
"hack": ApplyValueModificationsToTemplate("Attack/"+type+"/Hack", +(template.Attack[type].Hack || 0), player, template),
"pierce": ApplyValueModificationsToTemplate("Attack/"+type+"/Pierce", +(template.Attack[type].Pierce || 0), player, template),
"crush": ApplyValueModificationsToTemplate("Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0), player, template),
"minRange": ApplyValueModificationsToTemplate("Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0), player, template),
"maxRange": ApplyValueModificationsToTemplate("Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange, player, template),
"elevationBonus": ApplyValueModificationsToTemplate("Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0), player, template),
};
}
}
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 = ApplyValueModificationsToTemplate("Cost/Resources/food", +template.Cost.Resources.food, player, template);
if (template.Cost.Resources.wood) ret.cost.wood = ApplyValueModificationsToTemplate("Cost/Resources/wood", +template.Cost.Resources.wood, player, template);
if (template.Cost.Resources.stone) ret.cost.stone = ApplyValueModificationsToTemplate("Cost/Resources/stone", +template.Cost.Resources.stone, player, template);
if (template.Cost.Resources.metal) ret.cost.metal = ApplyValueModificationsToTemplate("Cost/Resources/metal", +template.Cost.Resources.metal, player, template);
if (template.Cost.Population) ret.cost.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, player, template);
if (template.Cost.PopulationBonus) ret.cost.populationBonus = ApplyValueModificationsToTemplate("Cost/PopulationBonus", +template.Cost.PopulationBonus, player, template);
if (template.Cost.BuildTime) ret.cost.time = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, player, template);
}
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": ApplyValueModificationsToTemplate("Pack/Time", +template.Pack.Time, player, template),
};
}
if (template.Health)
{
ret.health = Math.round(ApplyValueModificationsToTemplate("Health/Max", +template.Health.Max, player, template));
}
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": ApplyValueModificationsToTemplate("UnitMotion/WalkSpeed", +template.UnitMotion.WalkSpeed, player, template),
};
if (template.UnitMotion.Run) ret.speed.run = ApplyValueModificationsToTemplate("UnitMotion/Run/Speed", +template.UnitMotion.Run.Speed, player, template);
}
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();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
var cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
return cmpAttackDetection.GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, amounts)
{
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)
{
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
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for each (var posi in cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({'x': posi.x, 'y': posi.z});
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else 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.IsMapRevealed = function(player)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetLosRevealAll(-1);
};
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.GetTraderNumber = function(player)
+{
+ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ var traders = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) {
+ return Engine.QueryInterface(e, IID_Trader);
+ });
+
+ var landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
+ var shipTrader = { "total": 0, "trading": 0 };
+ for each (var ent in traders)
+ {
+ var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
+ var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ if (!cmpIdentity || !cmpUnitAI)
+ continue;
+ if (cmpIdentity.HasClass("Ship"))
+ {
+ ++shipTrader.total;
+ if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
+ ++shipTrader.trading;
+ }
+ else
+ {
+ ++landTrader.total;
+ if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
+ ++landTrader.trading;
+ if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
+ {
+ var holder = cmpUnitAI.order.data.target;
+ var cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
+ if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
+ ++landTrader.garrisoned;
+ }
+ }
+ }
+
+ return { "landTrader": landTrader, "shipTrader": shipTrader };
+};
+
+GuiInterface.prototype.GetTradingGoods = function(player, tradingGoods)
+{
+ var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
+ return cmpPlayer.GetTradingGoods();
+};
+
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
}
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
var exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetExtendedEntityState": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"GetTechnologyData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"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,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
+
+ "GetTraderNumber": 1,
+ "GetTradingGoods": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
else
throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 14417)
@@ -1,626 +1,668 @@
function Player() {}
Player.prototype.Schema =
"";
Player.prototype.Init = function()
{
this.playerID = undefined;
this.name = undefined; // define defaults elsewhere (supporting other languages)
this.civ = undefined;
this.colour = { "r": 0.0, "g": 0.0, "b": 0.0, "a": 1.0 };
this.popUsed = 0; // population of units owned or trained by this player
this.popBonuses = 0; // sum of population bonuses of player's entities
this.maxPop = 300; // maximum population
this.trainingBlocked = false; // indicates whether any training queue is currently blocked
this.resourceCount = {
"food": 300,
"wood": 300,
"metal": 300,
"stone": 300
};
-
+ this.tradingGoods = [ // goods for next trade-route and its proba in % (the sum of probas must be 100)
+ { "goods": "wood", "proba": 30 },
+ { "goods": "stone", "proba": 35 },
+ { "goods": "metal", "proba": 35 } ];
this.team = -1; // team number of the player, players on the same team will always have ally diplomatic status - also this is useful for team emblems, scoring, etc.
this.teamsLocked = false;
this.state = "active"; // game state - one of "active", "defeated", "won"
this.diplomacy = []; // array of diplomatic stances for this player with respect to other players (including gaia and self)
this.conquestCriticalEntitiesCount = 0; // number of owned units with ConquestCritical class
this.phase = "village";
this.formations = [];
this.startCam = undefined;
this.controlAllUnits = false;
this.isAI = false;
this.gatherRateMultiplier = 1;
this.cheatsEnabled = false;
this.cheatTimeMultiplier = 1;
this.heroes = [];
Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).CheckPlayers();
};
Player.prototype.SetPlayerID = function(id)
{
this.playerID = id;
};
Player.prototype.GetPlayerID = function()
{
return this.playerID;
};
Player.prototype.SetName = function(name)
{
this.name = name;
};
Player.prototype.GetName = function()
{
return this.name;
};
Player.prototype.SetCiv = function(civcode)
{
this.civ = civcode;
};
Player.prototype.GetCiv = function()
{
return this.civ;
};
Player.prototype.SetColour = function(r, g, b)
{
this.colour = { "r": r/255.0, "g": g/255.0, "b": b/255.0, "a": 1.0 };
};
Player.prototype.GetColour = function()
{
return this.colour;
};
// Try reserving num population slots. Returns 0 on success or number of missing slots otherwise.
Player.prototype.TryReservePopulationSlots = function(num)
{
if (num != 0 && num > (this.GetPopulationLimit() - this.GetPopulationCount()))
return num - (this.GetPopulationLimit() - this.GetPopulationCount());
this.popUsed += num;
return 0;
};
Player.prototype.UnReservePopulationSlots = function(num)
{
this.popUsed -= num;
};
Player.prototype.GetPopulationCount = function()
{
return this.popUsed;
};
Player.prototype.SetPopulationBonuses = function(num)
{
this.popBonuses = num;
};
Player.prototype.GetPopulationLimit = function()
{
return Math.min(this.GetMaxPopulation(), this.popBonuses);
};
Player.prototype.SetMaxPopulation = function(max)
{
this.maxPop = max;
};
Player.prototype.GetMaxPopulation = function()
{
return Math.round(ApplyValueModificationsToPlayer("Player/MaxPopulation", this.maxPop, this.entity));
};
Player.prototype.SetGatherRateMultiplier = function(value)
{
this.gatherRateMultiplier = value;
};
Player.prototype.GetGatherRateMultiplier = function()
{
return this.gatherRateMultiplier;
};
Player.prototype.GetHeroes = function()
{
return this.heroes;
};
Player.prototype.IsTrainingBlocked = function()
{
return this.trainingBlocked;
};
Player.prototype.BlockTraining = function()
{
this.trainingBlocked = true;
};
Player.prototype.UnBlockTraining = function()
{
this.trainingBlocked = false;
};
Player.prototype.SetResourceCounts = function(resources)
{
if (resources.food !== undefined)
this.resourceCount.food = resources.food;
if (resources.wood !== undefined)
this.resourceCount.wood = resources.wood;
if (resources.stone !== undefined)
this.resourceCount.stone = resources.stone;
if (resources.metal !== undefined)
this.resourceCount.metal = resources.metal;
};
Player.prototype.GetResourceCounts = function()
{
return this.resourceCount;
};
/**
* Add resource of specified type to player
* @param type Generic type of resource (string)
* @param amount Amount of resource, which should be added (integer)
*/
Player.prototype.AddResource = function(type, amount)
{
this.resourceCount[type] += (+amount);
};
/**
* Add resources to player
*/
Player.prototype.AddResources = function(amounts)
{
for (var type in amounts)
{
this.resourceCount[type] += (+amounts[type]);
}
};
Player.prototype.GetNeededResources = function(amounts)
{
// Check if we can afford it all
var amountsNeeded = {};
for (var type in amounts)
if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type])
amountsNeeded[type] = amounts[type] - this.resourceCount[type];
if (Object.keys(amountsNeeded).length == 0)
return undefined;
return amountsNeeded;
};
Player.prototype.SubtractResourcesOrNotify = function(amounts)
{
var amountsNeeded = this.GetNeededResources(amounts);
// If we don't have enough resources, send a notification to the player
if (amountsNeeded)
{
var formatted = [];
for (var type in amountsNeeded)
formatted.push(amountsNeeded[type] + " " + type[0].toUpperCase() + type.substr(1) );
var notification = {"player": this.playerID, "message": "Insufficient resources - " + formatted.join(", ")};
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
return false;
}
// Subtract the resources
for (var type in amounts)
this.resourceCount[type] -= amounts[type];
return true;
};
Player.prototype.TrySubtractResources = function(amounts)
{
if (!this.SubtractResourcesOrNotify(amounts))
return false;
var cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
if (cmpStatisticsTracker)
for (var type in amounts)
cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]);
return true;
};
+Player.prototype.GetNextTradingGoods = function()
+{
+ var value = 100*Math.random();
+ var last = this.tradingGoods.length - 1;
+ var sumProba = 0;
+ for (var i = 0; i < last; ++i)
+ {
+ sumProba += this.tradingGoods[i].proba;
+ if (value < sumProba)
+ return this.tradingGoods[i].goods;
+ }
+ return this.tradingGoods[last].goods;
+};
+
+Player.prototype.GetTradingGoods = function()
+{
+ var tradingGoods = {};
+ for each (var resource in this.tradingGoods)
+ tradingGoods[resource.goods] = resource.proba;
+
+ return tradingGoods;
+};
+
+Player.prototype.SetTradingGoods = function(tradingGoods)
+{
+ var sumProba = 0;
+ for (var resource in tradingGoods)
+ sumProba += tradingGoods[resource];
+ if (sumProba != 100) // consistency check
+ {
+ error("Player.js SetTradingGoods: " + uneval(tradingGoods));
+ tradingGoods = { "food": 20, "wood":20, "stone":30, "metal":30 };
+ }
+
+ this.tradingGoods = [];
+ for (var resource in tradingGoods)
+ this.tradingGoods.push( {"goods": resource, "proba": tradingGoods[resource]} );
+};
+
Player.prototype.GetState = function()
{
return this.state;
};
Player.prototype.SetState = function(newState)
{
this.state = newState;
};
Player.prototype.GetConquestCriticalEntitiesCount = function()
{
return this.conquestCriticalEntitiesCount;
};
Player.prototype.GetTeam = function()
{
return this.team;
};
Player.prototype.SetTeam = function(team)
{
if (!this.teamsLocked)
{
this.team = team;
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (cmpPlayerManager && this.team != -1)
{
// Set all team members as allies
for (var i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i)
{
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(i), IID_Player);
if (this.team == cmpPlayer.GetTeam())
{
this.SetAlly(i);
cmpPlayer.SetAlly(this.playerID);
}
}
}
Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID});
}
};
Player.prototype.SetLockTeams = function(value)
{
this.teamsLocked = value;
};
Player.prototype.GetLockTeams = function()
{
return this.teamsLocked;
};
Player.prototype.GetDiplomacy = function()
{
return this.diplomacy;
};
Player.prototype.SetDiplomacy = function(dipl)
{
// Should we check for teamsLocked here?
this.diplomacy = dipl;
Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID});
};
Player.prototype.SetDiplomacyIndex = function(idx, value)
{
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerManager)
return;
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(idx), IID_Player);
if (!cmpPlayer)
return;
if (this.state != "active" || cmpPlayer.state != "active")
return;
// You can have alliances with other players,
if (this.teamsLocked)
{
// but can't stab your team members in the back
if (this.team == -1 || cmpPlayer && this.team != cmpPlayer.GetTeam())
{
// Break alliance or declare war
if (Math.min(this.diplomacy[idx],cmpPlayer.diplomacy[this.playerID]) > value)
{
this.diplomacy[idx] = value;
cmpPlayer.SetDiplomacyIndex(this.playerID, value);
}
else
{
this.diplomacy[idx] = value;
}
Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID});
}
}
else
{
// Break alliance or declare war (worsening of relations is mutual)
if (Math.min(this.diplomacy[idx],cmpPlayer.diplomacy[this.playerID]) > value)
{
// This is duplicated because otherwise we get too much recursion
this.diplomacy[idx] = value;
cmpPlayer.SetDiplomacyIndex(this.playerID, value);
}
else
{
this.diplomacy[idx] = value;
}
Engine.BroadcastMessage(MT_DiplomacyChanged, {"player": this.playerID});
}
};
Player.prototype.UpdateSharedLos = function()
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
return;
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerManager)
return;
var sharedLos = [];
for (var i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i)
if (this.IsMutualAlly(i))
sharedLos.push(i);
cmpRangeManager.SetSharedLos(this.playerID, sharedLos);
};
Player.prototype.GetFormations = function()
{
return this.formations;
};
Player.prototype.SetFormations = function(formations)
{
this.formations = formations;
};
Player.prototype.GetStartingCameraPos = function()
{
return this.startCam.position;
};
Player.prototype.GetStartingCameraRot = function()
{
return this.startCam.rotation;
};
Player.prototype.SetStartingCamera = function(pos, rot)
{
this.startCam = {"position": pos, "rotation": rot};
};
Player.prototype.HasStartingCamera = function()
{
return (this.startCam !== undefined);
};
Player.prototype.SetControlAllUnits = function(c)
{
this.controlAllUnits = c;
};
Player.prototype.CanControlAllUnits = function()
{
return this.controlAllUnits;
};
Player.prototype.SetAI = function(flag)
{
this.isAI = flag;
};
Player.prototype.IsAI = function()
{
return this.isAI;
};
Player.prototype.SetAlly = function(id)
{
this.SetDiplomacyIndex(id, 1);
};
/**
* Check if given player is our ally
*/
Player.prototype.IsAlly = function(id)
{
return this.diplomacy[id] > 0;
};
/**
* Check if given player is our ally, and we are its ally
*/
Player.prototype.IsMutualAlly = function(id)
{
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerManager)
return false;
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(id), IID_Player);
return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID);
};
Player.prototype.SetEnemy = function(id)
{
this.SetDiplomacyIndex(id, -1);
};
/**
* Get all enemies of a given player.
*/
Player.prototype.GetEnemies = function()
{
var enemies = [];
for (var i = 0; i < this.diplomacy.length; i++)
if (this.diplomacy[i] < 0)
enemies.push(i);
return enemies;
};
/**
* Check if given player is our enemy
*/
Player.prototype.IsEnemy = function(id)
{
return this.diplomacy[id] < 0;
};
Player.prototype.SetNeutral = function(id)
{
this.SetDiplomacyIndex(id, 0);
};
/**
* Check if given player is neutral
*/
Player.prototype.IsNeutral = function(id)
{
return this.diplomacy[id] == 0;
};
/**
* Keep track of population effects of all entities that
* become owned or unowned by this player
*/
Player.prototype.OnGlobalOwnershipChanged = function(msg)
{
if (msg.from != this.playerID && msg.to != this.playerID)
return;
var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
var cmpCost = Engine.QueryInterface(msg.entity, IID_Cost);
if (msg.from == this.playerID)
{
if (cmpIdentity && cmpIdentity.HasClass("ConquestCritical"))
this.conquestCriticalEntitiesCount--;
if (this.conquestCriticalEntitiesCount == 0) // end game when needed
Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).CheckPlayers();
if (cmpCost)
{
this.popUsed -= cmpCost.GetPopCost();
this.popBonuses -= cmpCost.GetPopBonus();
}
if (cmpIdentity && cmpIdentity.HasClass("Hero"))
{
//Remove from Heroes list
var index = this.heroes.indexOf(msg.entity);
if (index >= 0)
this.heroes.splice(index, 1);
}
}
if (msg.to == this.playerID)
{
if (cmpIdentity && cmpIdentity.HasClass("ConquestCritical"))
this.conquestCriticalEntitiesCount++;
if (cmpCost)
{
this.popUsed += cmpCost.GetPopCost();
this.popBonuses += cmpCost.GetPopBonus();
}
if (cmpIdentity && cmpIdentity.HasClass("Hero"))
this.heroes.push(msg.entity);
}
};
Player.prototype.OnPlayerDefeated = function(msg)
{
this.state = "defeated";
// TODO: Tribute all resources to this player's active allies (if any)
// Reassign all player's entities to Gaia
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID);
// The ownership change is done in two steps so that entities don't hit idle
// (and thus possibly look for "enemies" to attack) before nearby allies get
// converted to Gaia as well.
for each (var entity in entities)
{
var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership);
cmpOwnership.SetOwnerQuiet(0);
}
// With the real ownership change complete, send OwnershipChanged messages.
for each (var entity in entities)
Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity,
"from": this.playerID, "to": 0 });
// Reveal the map for this player.
cmpRangeManager.SetLosRevealAll(this.playerID, true);
// Send a chat message notifying of the player's defeat.
var notification = {"type": "defeat", "player": this.playerID};
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
};
Player.prototype.OnDiplomacyChanged = function()
{
this.UpdateSharedLos();
};
Player.prototype.SetCheatsEnabled = function(flag)
{
this.cheatsEnabled = flag;
};
Player.prototype.GetCheatsEnabled = function()
{
return this.cheatsEnabled;
};
Player.prototype.SetCheatTimeMultiplier = function(time)
{
this.cheatTimeMultiplier = time;
};
Player.prototype.GetCheatTimeMultiplier = function()
{
return this.cheatTimeMultiplier;
};
Player.prototype.TributeResource = function(player, amounts)
{
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerManager)
return;
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(player), IID_Player);
if (!cmpPlayer)
return;
if (this.state != "active" || cmpPlayer.state != "active")
return;
if (!this.SubtractResourcesOrNotify(amounts))
return;
cmpPlayer.AddResources(amounts);
var total = Object.keys(amounts).reduce(function (sum, type){ return sum + amounts[type]; }, 0);
var cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
if (cmpOurStatisticsTracker)
cmpOurStatisticsTracker.IncreaseTributesSentCounter(total);
var cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
if (cmpTheirStatisticsTracker)
cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total);
var notification = {"type": "tribute", "player": player, "player1": this.playerID, "amounts": amounts};
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (cmpGUIInterface)
cmpGUIInterface.PushNotification(notification);
};
Engine.RegisterComponentType(IID_Player, "Player", Player);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Trader.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Trader.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Trader.js (revision 14417)
@@ -1,262 +1,277 @@
// See helpers/TraderGain.js for the CalculateTaderGain() function which works out how many
// resources a trader gets
// Additional gain for ships for each garrisoned trader, in percents
const GARRISONED_TRADER_ADDITION = 20;
// Array of resource names
const RESOURCES = ["food", "wood", "stone", "metal"];
function Trader() {}
Trader.prototype.Schema =
"Lets the unit generate resouces while moving between markets (or docks in case of water trading)." +
"" +
"2.0" +
"1.0" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Trader.prototype.Init = function()
{
this.firstMarket = INVALID_ENTITY;
this.secondMarket = INVALID_ENTITY;
// Gain from one pass between markets
this.gain = null;
// Selected resource for trading
- this.preferredGoods = "metal";
+ this.requiredGoods = undefined;
// Currently carried goods
this.goods = { "type": null, "amount": null, "origin": null };
};
Trader.prototype.CalculateGain = function(firstMarket, secondMarket)
{
var gain = CalculateTraderGain(firstMarket, secondMarket, this.template, this.entity);
// For ship increase gain for each garrisoned trader
// Calculate this here to save passing unnecessary stuff into the CalculateTraderGain function
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (cmpIdentity && cmpIdentity.HasClass("Ship"))
{
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
var garrisonMultiplier = 1;
var garrisonedTradersCount = 0;
for each (var entity in cmpGarrisonHolder.GetEntities())
{
var cmpGarrisonedUnitTrader = Engine.QueryInterface(entity, IID_Trader);
if (cmpGarrisonedUnitTrader)
garrisonedTradersCount++;
}
garrisonMultiplier *= 1 + GARRISONED_TRADER_ADDITION * garrisonedTradersCount / 100;
if (gain.traderGain)
gain.traderGain = Math.round(garrisonMultiplier * gain.traderGain);
if (gain.market1Gain)
gain.market1Gain = Math.round(garrisonMultiplier * gain.market1Gain);
if (gain.market2Gain)
gain.market2Gain = Math.round(garrisonMultiplier * gain.market2Gain);
}
}
return gain;
};
Trader.prototype.GetGain = function()
{
return this.gain;
};
// Set target as target market.
// Return true if at least one of markets was changed.
Trader.prototype.SetTargetMarket = function(target, source)
{
// Check that target is a market
var cmpTargetIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpTargetIdentity)
return false;
if (!cmpTargetIdentity.HasClass("Market") && !cmpTargetIdentity.HasClass("NavalMarket"))
return false;
var marketsChanged = true;
if (source)
{
// Establish a trade route with both markets in one go.
cmpTargetIdentity = Engine.QueryInterface(source, IID_Identity);
if (!cmpTargetIdentity)
return false;
if (!cmpTargetIdentity.HasClass("Market") && !cmpTargetIdentity.HasClass("NavalMarket"))
return false;
this.firstMarket = source;
this.secondMarket = INVALID_ENTITY;
}
if (this.secondMarket)
{
// If we already have both markets - drop them
// and use the target as first market
this.firstMarket = target;
this.secondMarket = INVALID_ENTITY;
}
else if (this.firstMarket)
{
// If we have only one market and target is different from it,
// set the target as second one
if (target == this.firstMarket)
marketsChanged = false;
else
{
this.secondMarket = target;
this.gain = this.CalculateGain(this.firstMarket, this.secondMarket);
}
}
else
{
// Else we don't have target markets at all,
// set the target as first market
this.firstMarket = target;
}
if (marketsChanged)
{
// Drop carried goods
this.goods.amount = null;
}
return marketsChanged;
};
Trader.prototype.GetFirstMarket = function()
{
return this.firstMarket;
};
Trader.prototype.GetSecondMarket = function()
{
return this.secondMarket;
};
Trader.prototype.HasBothMarkets = function()
{
return this.firstMarket && this.secondMarket;
};
-Trader.prototype.GetPreferredGoods = function()
+Trader.prototype.GetRequiredGoods = function()
{
- return this.preferredGoods;
+ return this.requiredGoods;
};
-Trader.prototype.SetPreferredGoods = function(preferredGoods)
+Trader.prototype.SetRequiredGoods = function(requiredGoods)
{
// Check that argument is a correct resource name
- if (RESOURCES.indexOf(preferredGoods) == -1)
- return;
- this.preferredGoods = preferredGoods;
+ if (!requiredGoods || RESOURCES.indexOf(requiredGoods) == -1)
+ this.requiredGoods = undefined;
+ else
+ this.requiredGoods = requiredGoods;
};
Trader.prototype.CanTrade = function(target)
{
var cmpTraderIdentity = Engine.QueryInterface(this.entity, IID_Identity);
var cmpTargetIdentity = Engine.QueryInterface(target, IID_Identity);
// Check that the target exists
if (!cmpTargetIdentity)
return false;
// Check that the target is not a foundation
var cmpTargetFoundation = Engine.QueryInterface(target, IID_Foundation);
if (cmpTargetFoundation)
return false;
var landTradingPossible = cmpTraderIdentity.HasClass("Organic") && cmpTargetIdentity.HasClass("Market");
var seaTradingPossible = cmpTraderIdentity.HasClass("Ship") && cmpTargetIdentity.HasClass("NavalMarket");
if (!landTradingPossible && !seaTradingPossible)
return false;
var cmpTraderPlayer = QueryOwnerInterface(this.entity, IID_Player);
var traderPlayerId = cmpTraderPlayer.GetPlayerID();
var cmpTargetPlayer = QueryOwnerInterface(target, IID_Player);
var targetPlayerId = cmpTargetPlayer.GetPlayerID();
var ownershipSuitableForTrading = cmpTraderPlayer.IsAlly(targetPlayerId) || cmpTraderPlayer.IsNeutral(targetPlayerId);
if (!ownershipSuitableForTrading)
return false;
return true;
};
Trader.prototype.PerformTrade = function(currentMarket)
{
if (this.goods.amount && this.goods.amount.traderGain)
{
var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (cmpPlayer)
cmpPlayer.AddResource(this.goods.type, this.goods.amount.traderGain);
var cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseTradeIncomeCounter(this.goods.amount.traderGain);
if (this.goods.amount.market1Gain)
{
var cmpPlayer = QueryOwnerInterface(this.firstMarket, IID_Player);
if (cmpPlayer)
cmpPlayer.AddResource(this.goods.type, this.goods.amount.market1Gain);
var cmpStatisticsTracker = QueryOwnerInterface(this.firstMarket, IID_StatisticsTracker);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseTradeIncomeCounter(this.goods.amount.market1Gain);
}
if (this.goods.amount.market2Gain)
{
var cmpPlayer = QueryOwnerInterface(this.secondMarket, IID_Player);
if (cmpPlayer)
cmpPlayer.AddResource(this.goods.type, this.goods.amount.market2Gain);
var cmpStatisticsTracker = QueryOwnerInterface(this.secondMarket, IID_StatisticsTracker);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseTradeIncomeCounter(this.goods.amount.market2Gain);
}
}
- this.goods.type = this.preferredGoods;
+
+ // First take the preferred goods of the trader if any,
+ // otherwise choose one according to the player's trading priorities
+ // if still nothing (but should never happen), choose metal
+ var nextGoods = this.GetRequiredGoods();
+ if (!nextGoods || RESOURCES.indexOf(nextGoods) == -1)
+ {
+ var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+ if (cmpPlayer)
+ nextGoods = cmpPlayer.GetNextTradingGoods();
+
+ if (!nextGoods || RESOURCES.indexOf(nextGoods) == -1)
+ nextGoods = "metal";
+ }
+ this.goods.type = nextGoods;
this.goods.amount = this.gain;
this.goods.origin = currentMarket;
};
Trader.prototype.GetGoods = function()
{
return this.goods;
};
Trader.prototype.GetNextMarket = function()
{
if (this.goods.amount && this.goods.origin == this.firstMarket)
return this.secondMarket;
else
return this.firstMarket;
};
Trader.prototype.StopTrading = function()
{
// Drop carried goods
this.goods.amount = null;
// Reset markets
this.firstMarket = INVALID_ENTITY;
this.secondMarket = INVALID_ENTITY;
};
// Get range in which deals with market are available,
// i.e. trader should be in no more than MaxDistance from market
// to be able to trade with it.
Trader.prototype.GetRange = function()
{
return { "min": 0, "max": +this.template.MaxDistance };
};
Trader.prototype.OnGarrisonedUnitsChanged = function()
{
if (this.HasBothMarkets())
this.gain = this.CalculateGain(this.firstMarket, this.secondMarket);
};
Engine.RegisterComponentType(IID_Trader, "Trader", Trader);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 14416)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 14417)
@@ -1,1467 +1,1471 @@
// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
function ProcessCommand(player, cmd)
{
// Do some basic checks here that commanding player is valid
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerMan || player < 0)
return;
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
if (playerEnt == INVALID_ENTITY)
return;
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
if (!cmpPlayer)
return;
var controlAllUnits = cmpPlayer.CanControlAllUnits();
var entities;
if (cmd.entities)
entities = FilterEntityList(cmd.entities, player, controlAllUnits);
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// Now handle various commands
switch (cmd.type)
{
case "debug-print":
print(cmd.message);
break;
case "chat":
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": cmd.message});
break;
case "cheat":
Cheat(cmd);
break;
case "quit":
// Let the AI exit the game for testing purposes
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "quit"});
break;
case "diplomacy":
switch(cmd.to)
{
case "ally":
cmpPlayer.SetAlly(cmd.player);
break;
case "neutral":
cmpPlayer.SetNeutral(cmd.player);
break;
case "enemy":
cmpPlayer.SetEnemy(cmd.player);
break;
default:
warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "diplomacy", "player": player, "player1": cmd.player, "status": cmd.to});
break;
case "tribute":
cmpPlayer.TributeResource(cmd.player, cmd.amounts);
break;
case "control-all":
cmpPlayer.SetControlAllUnits(cmd.flag);
break;
case "reveal-map":
// Reveal the map for all players, not just the current player,
// primarily to make it obvious to everyone that the player is cheating
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
break;
case "walk":
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
});
break;
case "attack-walk":
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.queued);
});
break;
case "attack":
if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanAttack for target checks
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Attack(cmd.target, cmd.queued);
});
break;
case "heal":
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
}
// See UnitAI.CanHeal for target checks
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Heal(cmd.target, cmd.queued);
});
break;
case "repair":
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanRepair for target checks
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
});
break;
case "gather":
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
}
// See UnitAI.CanGather for target checks
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Gather(cmd.target, cmd.queued);
});
break;
case "gather-near-position":
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued);
});
break;
case "returnresource":
// Check dropsite is owned by player
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
}
// See UnitAI.CanReturnResource for target checks
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
});
break;
case "back-to-work":
for each (var ent in entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(!cmpUnitAI || !cmpUnitAI.BackToWork())
notifyBackToWorkFailure(player);
}
break;
case "remove-guard":
for each (var ent in entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(cmpUnitAI)
cmpUnitAI.RemoveGuard();
}
break;
case "train":
// Check entity limits
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(cmd.template);
var unitCategory = null;
if (template.TrainingRestrictions)
unitCategory = template.TrainingRestrictions.Category;
// Verify that the building(s) can be controlled by the player
if (entities.length > 0)
{
for each (var ent in entities)
{
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));
continue;
}
}
var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
// TODO: Enable this check once the AI gets technology support
if (cmpTechnologyManager.CanProduce(cmd.template) || cmpPlayer.IsAI())
{
var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
// Check if the building can train the unit
if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
queue.AddBatch(cmd.template, "unit", +cmd.count, cmd.metadata);
}
else if (g_DebugCommands)
{
warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
}
}
}
else if (g_DebugCommands)
{
warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "research":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.entity, player, controlAllUnits))
{
var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
// TODO: Enable this check once the AI gets technology support
if (cmpTechnologyManager.CanResearch(cmd.template) || cmpPlayer.IsAI())
{
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.AddBatch(cmd.template, "technology");
}
else if (g_DebugCommands)
{
warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
}
}
else if (g_DebugCommands)
{
warn("Invalid command: research building cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "stop-production":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.entity, player, controlAllUnits))
{
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.RemoveBatch(cmd.id);
}
else if (g_DebugCommands)
{
warn("Invalid command: production building cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "construct":
TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd);
break;
case "construct-wall":
TryConstructWall(player, cmpPlayer, controlAllUnits, cmd);
break;
case "delete-entities":
for each (var ent in entities)
{
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply || !cmpResourceSupply.GetKillBeforeGather())
cmpHealth.Kill();
}
else
Engine.DestroyEntity(ent);
}
break;
case "set-rallypoint":
for each (var ent in entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
if (!cmd.queued)
cmpRallyPoint.Unset();
cmpRallyPoint.AddPosition(cmd.x, cmd.z);
cmpRallyPoint.AddData(cmd.data);
}
}
break;
case "unset-rallypoint":
for each (var ent in entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Reset();
}
break;
case "defeat-player":
// Send "OnPlayerDefeated" message to player
Engine.PostMessage(playerEnt, MT_PlayerDefeated, { "playerId": player } );
break;
case "garrison":
// Verify that the building can be controlled by the player or is mutualAlly
if (CanControlUnitOrIsAlly(cmd.target, player, controlAllUnits))
{
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Garrison(cmd.target, cmd.queued);
});
}
else if (g_DebugCommands)
{
warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
}
break;
case "guard":
// Verify that the target can be controlled by the player or is mutualAlly
if (CanControlUnitOrIsAlly(cmd.target, player, controlAllUnits))
{
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Guard(cmd.target, cmd.queued);
});
}
else if (g_DebugCommands)
{
warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "stop":
GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Stop(cmd.queued);
});
break;
case "unload":
// Verify that the building can be controlled by the player or is mutualAlly
if (CanControlUnitOrIsAlly(cmd.garrisonHolder, player, controlAllUnits))
{
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
var notUngarrisoned = 0;
// The owner can ungarrison every garrisoned unit
if (IsOwnedByPlayer(player, cmd.garrisonHolder))
entities = cmd.entities;
for each (var ent in entities)
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
notUngarrisoned++;
if (notUngarrisoned != 0)
notifyUnloadFailure(player, cmd.garrisonHolder)
}
else if (g_DebugCommands)
{
warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
}
break;
case "unload-template":
var index = cmd.template.indexOf("&"); // Templates for garrisoned units are extended
if (index == -1)
break;
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
// Only the owner of the garrisonHolder may unload entities from any owners
if (!IsOwnedByPlayer(player, garrisonHolder) && !controlAllUnits
&& player != +cmd.template.slice(1,index))
continue;
if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.all))
notifyUnloadFailure(player, garrisonHolder);
}
}
break;
case "unload-all-own":
var entities = FilterEntityList(cmd.garrisonHolders, player, controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllOwn())
notifyUnloadFailure(player, garrisonHolder)
}
break;
case "unload-all":
var entities = FilterEntityList(cmd.garrisonHolders, player, controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
notifyUnloadFailure(player, garrisonHolder)
}
break;
case "increase-alert-level":
for each (var ent in entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (!cmpAlertRaiser || !cmpAlertRaiser.IncreaseAlertLevel())
notifyAlertFailure(player);
}
break;
case "alert-end":
for each (var ent in entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.EndOfAlert();
}
break;
case "formation":
GetFormationUnitAIs(entities, player, cmd.name).forEach(function(cmpUnitAI) {
cmpUnitAI.MoveIntoFormation(cmd);
});
break;
case "promote":
// No need to do checks here since this is a cheat anyway
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": "(Cheat - promoted units)"});
for each (var ent in cmd.entities)
{
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
}
break;
case "stance":
for each (var ent in entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SwitchToStance(cmd.name);
}
break;
case "wall-to-gate":
for each (var ent in entities)
{
TryTransformWallToGate(ent, cmpPlayer, cmd.template);
}
break;
case "lock-gate":
for each (var ent in entities)
{
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
{
if (cmd.lock)
cmpGate.LockGate();
else
cmpGate.UnlockGate();
}
}
break;
case "setup-trade-route":
for each (var ent in entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source);
}
break;
- case "select-trading-goods":
+ case "select-required-goods":
for each (var ent in entities)
{
var cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
- cmpTrader.SetPreferredGoods(cmd.preferredGoods);
+ cmpTrader.SetRequiredGoods(cmd.requiredGoods);
}
break;
+ case "set-trading-goods":
+ cmpPlayer.SetTradingGoods(cmd.tradingGoods);
+ break;
+
case "barter":
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
cmpBarter.ExchangeResources(playerEnt, cmd.sell, cmd.buy, cmd.amount);
break;
case "set-shading-color":
// Debug command to make an entity brightly colored
for each (var ent in cmd.entities)
{
var cmpVisual = Engine.QueryInterface(ent, IID_Visual)
if (cmpVisual)
cmpVisual.SetShadingColour(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0) // alpha isn't used so just send 0
}
break;
case "pack":
for each (var ent in entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmd.pack)
cmpUnitAI.Pack(cmd.queued);
else
cmpUnitAI.Unpack(cmd.queued);
}
}
break;
case "cancel-pack":
for each (var ent in entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmd.pack)
cmpUnitAI.CancelPack(cmd.queued);
else
cmpUnitAI.CancelUnpack(cmd.queued);
}
}
break;
default:
error("Invalid command: unknown command type: "+uneval(cmd));
}
}
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player, garrisonHolder)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Unable to ungarrison unit(s)" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Sends a GUI notification about worker(s) that failed to go back to work.
*/
function notifyBackToWorkFailure(player)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Some unit(s) can't go back to work" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Sends a GUI notification about Alerts that failed to be raised
*/
function notifyAlertFailure(player)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"player": cmpPlayer.GetPlayerID(), "message": "You can't raise the alert to a higher level !" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Get some information about the formations used by entities.
* The entities must have a UnitAI component.
*/
function ExtractFormations(ents)
{
var entities = []; // subset of ents that have UnitAI
var members = {}; // { formationentity: [ent, ent, ...], ... }
for each (var ent in ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var fid = cmpUnitAI.GetFormationController();
if (fid != INVALID_ENTITY)
{
if (!members[fid])
members[fid] = [];
members[fid].push(ent);
}
entities.push(ent);
}
var ids = [ id for (id in members) ];
return { "entities": entities, "members": members, "ids": ids };
}
/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
function GetDockAngle(templateName,x,y)
{
var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateMgr.GetTemplate(templateName);
if (template.BuildRestrictions.Category !== "Dock")
return undefined;
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
{
return undefined;
}
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
{
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
}
else if (template.Footprint.Circle)
{
halfSize = template.Footprint.Circle["@radius"];
}
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = x - d*Math.sin(angle);
var nz = y + 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 -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI);
}
}
return undefined;
}
/**
* Attempts to construct a building using the specified parameters.
* Returns true on success, false on failure.
*/
function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
{
// Message structure:
// {
// "type": "construct",
// "entities": [...], // entities that will be ordered to construct the building (if applicable)
// "template": "...", // template name of the entity being constructed
// "x": ...,
// "z": ...,
// "angle": ...,
// "metadata": "...", // AI metadata of the building
// "actorSeed": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
// "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
// "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. May be INVALID_ENTITY.
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check whether we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
return false;
var foundationTemplate = "foundation|" + cmd.template;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity(foundationTemplate);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
return false;
}
// If it's a dock, get the right angle.
var angle = GetDockAngle(cmd.template,cmd.x,cmd.z);
if (angle !== undefined)
cmd.angle = angle;
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(cmd.angle);
// Set the obstruction control group if needed
if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
{
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
// primary control group must always be valid
if (cmd.obstructionControlGroup)
{
if (cmd.obstructionControlGroup <= 0)
warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
}
if (cmd.obstructionControlGroup2)
cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (cmpBuildRestrictions)
{
var ret = cmpBuildRestrictions.CheckPlacement();
if (!ret.success)
{
if (g_DebugCommands)
{
warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": ret.message });
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
return false;
}
}
else
error("cmpBuildRestrictions not defined");
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (!cmpEntityLimits || !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
{
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
}
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
return false;
}
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
// TODO: Enable this check once the AI gets technology support
if (!cmpTechnologyManager.CanProduce(cmd.template) && !cmpPlayer.IsAI())
{
if (g_DebugCommands)
{
warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": "Building's technology requirements are not met." });
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
}
// We need the cost after tech modifications
// To calculate this with an entity requires ownership, so use the template instead
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(foundationTemplate);
var costs = {};
for (var r in template.Cost.Resources)
{
costs[r] = +template.Cost.Resources[r];
if (cmpTechnologyManager)
costs[r] = cmpTechnologyManager.ApplyModificationsTemplate("Cost/Resources/"+r, costs[r], template);
}
if (!cmpPlayer.TrySubtractResources(costs))
{
if (g_DebugCommands)
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(ent);
return false;
}
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual && cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// send Metadata info if any
if (cmd.metadata)
Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued
});
}
return ent;
}
function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
{
// 'cmd' message structure:
// {
// "type": "construct-wall",
// "entities": [...], // entities that will be ordered to construct the wall (if applicable)
// "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
// {
// "template": "...", // one of the templates from the wallset
// "x": ...,
// "z": ...,
// "angle": ...,
// },
// ...
// ],
// "wallSet": {
// "templates": {
// "tower": // tower template name
// "long": // long wall segment template name
// ... // etc.
// },
// "maxTowerOverlap": ...,
// "minTowerOverlap": ...,
// },
// "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall
// "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
// }
if (cmd.pieces.length <= 0)
return;
if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
return;
}
if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
return;
}
// Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
// and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
// groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
// towers in the case of snapping). The towers themselves all keep their default unique control groups.
// To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
// it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
// first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
// the first tower encountered towards the ending side of the wall (if any).
// We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
// wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
//
// FIRST PASS:
// - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
// cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
// as the primary control group, thus allowing it to be built overlapping the previous piece.
// - If we encounter a new tower along the way (which will gain its own control group), do the following:
// o First build it using temporarily the same control group of the previous (non-tower) piece
// o Set the previous piece's secondary control group to the tower's entity ID
// o Restore the primary control group of the constructed tower back its original (unique) value.
// The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
// to be placed while overlapping the previous piece.
//
// SECOND PASS:
// - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
// time registering the right neighbouring tower in each non-tower piece.
// first pass; L -> R
var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
// the first wall piece can be built while overlapping it.
if (cmd.startSnappedEntity)
{
var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
if (!cmpSnappedStartObstruction)
{
error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
//warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
}
var i = 0;
for (; i < cmd.pieces.length; ++i)
{
var piece = cmd.pieces[i];
// All wall pieces after the first must be queued.
if (i > 0 && !cmd.queued)
cmd.queued = true;
// 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
// start position snapping (implying that the first entity we build must be a tower)
if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
{
if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
{
error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
break;
}
}
var constructPieceCmd = {
"type": "construct",
"entities": cmd.entities,
"template": piece.template,
"x": piece.x,
"z": piece.z,
"angle": piece.angle,
"autorepair": cmd.autorepair,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued,
// Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
// using the control group of the last tower (see comments above).
"obstructionControlGroup": lastTowerControlGroup,
};
// If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
// control group directly at construction time (instead of setting it in the second pass) to allow it to be built
// while overlapping the snapped entity.
if (i == cmd.pieces.length - 1 && cmd.endSnappedEntity)
{
var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (cmpEndSnappedObstruction)
constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
}
var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
if (pieceEntityId)
{
// wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
piece.ent = pieceEntityId;
// if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
if (piece.template == cmd.wallSet.templates.tower)
{
var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
var newTowerControlGroup = pieceEntityId;
if (i > 0)
{
//warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup);
var cmpPreviousObstruction = Engine.QueryInterface(cmd.pieces[i-1].ent, IID_Obstruction);
// TODO: ensure that cmpPreviousObstruction exists
// TODO: ensure that the previous obstruction does not yet have a secondary control group set
cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
}
// TODO: ensure that cmpTowerObstruction exists
cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
lastTowerIndex = i;
lastTowerControlGroup = newTowerControlGroup;
}
}
else
{
// failed to build wall piece, abort
i = j + 1; // compensate for the -1 subtracted by lastBuiltPieceIndex below
break;
}
}
var lastBuiltPieceIndex = i - 1;
var wallComplete = (lastBuiltPieceIndex == cmd.pieces.length - 1);
// At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
// Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
// as their secondary control groups.
lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// only start off with the ending side's snapped tower's control group if we were able to build the entire wall
if (cmd.endSnappedEntity && wallComplete)
{
var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (!cmpSnappedEndObstruction)
{
error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
}
for (var j = lastBuiltPieceIndex; j >= 0; --j)
{
var piece = cmd.pieces[j];
if (!piece.ent)
{
error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
continue;
}
var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
if (!cmpPieceObstruction)
{
error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
continue;
}
if (piece.template == cmd.wallSet.templates.tower)
{
// encountered a tower entity, update the last tower control group
lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
}
else
{
// Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
// Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
// dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
if (existingSecondaryControlGroup == INVALID_ENTITY)
{
if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
{
cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
}
}
else if (existingSecondaryControlGroup != lastTowerControlGroup)
{
error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
break;
}
}
}
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
var formation = ExtractFormations(ents);
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents, player, formName)
{
// If an individual was selected, remove it from any formation
// and command it individually
if (ents.length == 1)
{
// Skip unit if it has no UnitAI
var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
RemoveFromFormation(ents);
return [ cmpUnitAI ];
}
// Separate out the units that don't support the chosen formation
var formedEnts = [];
var nonformedUnitAIs = [];
for each (var ent in ents)
{
// Skip units with no UnitAI
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
// We only use "LineClosed" instead of "Line Closed" to access the templates.
if (cmpIdentity && cmpIdentity.CanUseFormation(formName === undefined ? "LineClosed" : formName.replace(/\s+/,'')))
formedEnts.push(ent);
else
nonformedUnitAIs.push(cmpUnitAI);
}
if (formedEnts.length == 0)
{
// No units support the foundation - return all the others
return nonformedUnitAIs;
}
// Find what formations the formationable selected entities are currently in
var formation = ExtractFormations(formedEnts);
var formationEnt = undefined;
if (formation.ids.length == 1)
{
// Selected units either belong to this formation or have no formation
// Check that all its members are selected
var fid = formation.ids[0];
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length
&& cmpFormation.GetMemberCount() == formation.entities.length)
{
// The whole formation was selected, so reuse its controller for this command
formationEnt = +fid;
}
}
if (!formationEnt)
{
// We need to give the selected units a new formation controller
// Remove selected units from their current formation
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
// Create the new controller
formationEnt = Engine.AddEntity("special/formation");
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
cmpFormation.SetMembers(formation.entities);
var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
// If all the selected units were previously in formations of the same shape,
// then set this new formation to that shape too; otherwise use the default shape
var lastFormationName = undefined;
for each (var ent in formation.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
var name = cmpUnitAI.GetLastFormationName();
if (lastFormationName === undefined)
{
lastFormationName = name;
}
else if (lastFormationName != name)
{
lastFormationName = undefined;
break;
}
}
}
var formationName;
if (lastFormationName)
formationName = lastFormationName;
else
formationName = "Line Closed";
if (CanMoveEntsIntoFormation(formation.entities, formationName))
{
cmpFormation.LoadFormation(formationName);
}
else
{
cmpFormation.LoadFormation("Scatter");
}
}
return nonformedUnitAIs.concat(Engine.QueryInterface(formationEnt, IID_UnitAI));
}
function GetFormationRequirements(formationName)
{
var countRequired = 1;
var classesRequired;
switch(formationName)
{
case "Scatter":
case "Column Closed":
case "Line Closed":
case "Column Open":
case "Line Open":
case "Battle Line":
break;
case "Box":
countRequired = 4;
break;
case "Flank":
countRequired = 8;
break;
case "Skirmish":
classesRequired = ["Ranged"];
break;
case "Wedge":
countRequired = 3;
classesRequired = ["Cavalry"];
break;
case "Phalanx":
countRequired = 10;
classesRequired = ["Melee", "Infantry"];
break;
case "Syntagma":
countRequired = 9;
classesRequired = ["Melee", "Infantry"]; // TODO: pike only
break;
case "Testudo":
countRequired = 9;
classesRequired = ["Melee", "Infantry"];
break;
default:
// We encountered a unknown formation -> warn the user
warn("Commands.js: GetFormationRequirements: unknown formation: " + formationName);
return false;
}
return { "count": countRequired, "classesRequired": classesRequired };
}
function CanMoveEntsIntoFormation(ents, formationName)
{
// TODO: should check the player's civ is allowed to use this formation
// See simulation/components/Player.js GetFormations() for a list of all allowed formations
var requirements = GetFormationRequirements(formationName);
if (!requirements)
return false;
formationName = formationName.replace(/\s+/g,"");
var count = 0;
var reqClasses = requirements.classesRequired || [];
for each (var ent in ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationName))
continue;
var classes = cmpIdentity.GetClassesList();
if (reqClasses.some(function(c){return classes.indexOf(c) == -1;}))
return false;
count++;
}
if (count < requirements.count)
return false;
return true;
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player
* or control all units is activated, else false
*/
function CanControlUnit(entity, player, controlAll)
{
return (IsOwnedByPlayer(player, entity) || controlAll);
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player
* or the entity is owned by an mutualAlly
* or control all units is activated, else false
*/
function CanControlUnitOrIsAlly(entity, player, controlAll)
{
return (IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll);
}
/**
* Filter entities which the player can control
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(function(ent) { return CanControlUnit(ent, player, controlAll);} );
}
/**
* Filter entities which the player can control or are mutualAlly
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
return entities.filter(function(ent) { return CanControlUnitOrIsAlly(ent, player, controlAll);} );
}
/**
* Try to transform a wall to a gate
*/
function TryTransformWallToGate(ent, cmpPlayer, template)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
return;
// Check if this is a valid long wall segment
if (!cmpIdentity.HasClass("LongWall"))
{
if (g_DebugCommands)
warn("Invalid command: invalid wall conversion to gate for player "+player+": "+uneval(cmd));
return;
}
var civ = cmpIdentity.GetCiv();
var gate = Engine.AddEntity(template);
var cmpCost = Engine.QueryInterface(gate, IID_Cost);
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
if (g_DebugCommands)
warn("Invalid command: convert gate cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(gate);
return;
}
ReplaceBuildingWith(ent, gate);
}
/**
* Unconditionally replace a building with another one
*/
function ReplaceBuildingWith(ent, building)
{
// Move the building to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position);
var pos = cmpPosition.GetPosition2D();
cmpBuildingPosition.JumpTo(pos.x, pos.y);
var rot = cmpPosition.GetRotation();
cmpBuildingPosition.SetYRotation(rot.y);
cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
// Copy ownership
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
// Copy control groups
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
// Copy health level from the old entity to the new
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
var healthFraction = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
var buildingHitpoints = Math.round(cmpBuildingHealth.GetMaxHitpoints() * healthFraction);
cmpBuildingHealth.SetHitpoints(buildingHitpoints);
PlaySound("constructed", building);
Engine.PostMessage(ent, MT_ConstructionFinished,
{ "entity": ent, "newentity": building });
Engine.BroadcastMessage(MT_EntityRenamed, { entity: ent, newentity: building });
Engine.DestroyEntity(ent);
}
Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);