Wall placement. Closes #786.

This was SVN commit r11760.
This commit is contained in:
vts 2012-05-05 19:22:22 +00:00
parent a6a7883af3
commit 490182ddd0
95 changed files with 2874 additions and 340 deletions

View File

@ -17,21 +17,19 @@ const ACTION_GARRISON = 1;
const ACTION_REPAIR = 2;
var preSelectedAction = ACTION_NONE;
var INPUT_NORMAL = 0;
var INPUT_SELECTING = 1;
var INPUT_BANDBOXING = 2;
var INPUT_BUILDING_PLACEMENT = 3;
var INPUT_BUILDING_CLICK = 4;
var INPUT_BUILDING_DRAG = 5;
var INPUT_BATCHTRAINING = 6;
var INPUT_PRESELECTEDACTION = 7;
const INPUT_NORMAL = 0;
const INPUT_SELECTING = 1;
const INPUT_BANDBOXING = 2;
const INPUT_BUILDING_PLACEMENT = 3;
const INPUT_BUILDING_CLICK = 4;
const INPUT_BUILDING_DRAG = 5;
const INPUT_BATCHTRAINING = 6;
const INPUT_PRESELECTEDACTION = 7;
const INPUT_BUILDING_WALL_CLICK = 8;
const INPUT_BUILDING_WALL_PATHING = 9;
var inputState = INPUT_NORMAL;
var defaultPlacementAngle = Math.PI*3/4;
var placementAngle = undefined;
var placementPosition = undefined;
var placementEntity = undefined;
var placementSupport = new PlacementSupport();
var mouseX = 0;
var mouseY = 0;
@ -83,34 +81,62 @@ function updateCursorAndTooltip()
Engine.SetCursor("arrow-default");
if (!tooltipSet)
informationTooltip.hidden = true;
var wallDragTooltip = getGUIObjectByName("wallDragTooltip");
if (placementSupport.wallDragTooltip)
{
wallDragTooltip.caption = placementSupport.wallDragTooltip;
wallDragTooltip.hidden = false;
}
else
{
wallDragTooltip.caption = "";
wallDragTooltip.hidden = true;
}
}
function updateBuildingPlacementPreview()
{
// The preview should be recomputed every turn, so that it responds
// to obstructions/fog/etc moving underneath it
// 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 (placementEntity && placementPosition)
if (placementSupport.mode === "building")
{
return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementEntity,
"x": placementPosition.x,
"z": placementPosition.z,
"angle": placementAngle
});
if (placementSupport.template && placementSupport.position)
{
return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
});
}
}
else if (placementSupport.mode === "wall")
{
if (placementSupport.wallSet && placementSupport.position)
{
// Fetch an updated list of snapping candidate entities
placementSupport.wallSnapEntities = Engine.PickSimilarFriendlyEntities(
placementSupport.wallSet.templates.tower,
placementSupport.wallSnapEntitiesIncludeOffscreen,
true, // require exact template match
true // include foundations
);
return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
"wallSet": placementSupport.wallSet,
"start": placementSupport.position,
"end": placementSupport.wallEndPosition,
"snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment
});
}
}
return false;
}
function resetPlacementEntity()
{
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
placementEntity = undefined;
placementPosition = undefined;
placementAngle = undefined;
}
function findGatherType(gatherer, supply)
{
if (!gatherer || !supply)
@ -459,6 +485,12 @@ var dragStart; // used for remembering mouse coordinates at start of drag operat
function tryPlaceBuilding(queued)
{
if (placementSupport.mode !== "building")
{
error("[tryPlaceBuilding] Called while in '"+placementSupport.mode+"' placement mode instead of 'building'");
return false;
}
var selection = g_Selection.toList();
// Use the preview to check it's a valid build location
@ -472,10 +504,10 @@ function tryPlaceBuilding(queued)
// Start the construction
Engine.PostNetworkCommand({
"type": "construct",
"template": placementEntity,
"x": placementPosition.x,
"z": placementPosition.z,
"angle": placementAngle,
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"entities": selection,
"autorepair": true,
"autocontinue": true,
@ -484,7 +516,60 @@ function tryPlaceBuilding(queued)
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
if (!queued)
resetPlacementEntity();
placementSupport.Reset();
return true;
}
function tryPlaceWall()
{
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": true,
"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;
}
@ -684,7 +769,34 @@ function handleInputBeforeGui(ev, hoveredObject)
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
resetPlacementEntity();
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;
}
@ -692,6 +804,81 @@ function handleInputBeforeGui(ev, hoveredObject)
}
break;
case INPUT_BUILDING_WALL_PATHING:
// User has chosen a starting point for constructing the wall, and is now looking to set the endpoint.
// Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to
// normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the
// user to continue building walls.
switch (ev.type)
{
case "mousemotion":
placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
// Update the building placement preview, and by extension, the list of snapping candidate entities for both (!)
// the ending point and the starting point to snap to.
//
// TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case
// where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a
// foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on
// the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers
// in them. Might be useful to query only for entities within a certain range around the starting point and ending
// points.
placementSupport.wallSnapEntitiesIncludeOffscreen = true;
var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
if (result && result.cost)
{
placementSupport.wallDragTooltip = "";
for (var resource in result.cost)
{
if (result.cost[resource] > 0)
placementSupport.wallDragTooltip += getCostComponentDisplayName(resource) + ": " + result.cost[resource] + "\n";
}
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
if (tryPlaceWall())
{
if (Engine.HotkeyIsPressed("session.queue"))
{
// continue building, just set a new starting position where we left off
placementSupport.position = placementSupport.wallEndPosition;
placementSupport.wallEndPosition = undefined;
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
}
}
else
{
placementSupport.wallDragTooltip = "Cannot build wall here!";
}
updateBuildingPlacementPreview();
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// reset to normal input mode
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_DRAG:
switch (ev.type)
{
@ -702,25 +889,25 @@ function handleInputBeforeGui(ev, hoveredObject)
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
// Rotate in the direction of the mouse
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
placementAngle = Math.atan2(target.x - placementPosition.x, target.z - placementPosition.z);
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
placementAngle = defaultPlacementAngle;
placementSupport.SetDefaultAngle();
}
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementEntity,
"x": placementPosition.x,
"z": placementPosition.z
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z
});
if (snapData)
{
placementAngle = snapData.angle;
placementPosition.x = snapData.x;
placementPosition.z = snapData.z;
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
updateBuildingPlacementPreview();
@ -750,7 +937,7 @@ function handleInputBeforeGui(ev, hoveredObject)
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
resetPlacementEntity();
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
@ -844,6 +1031,7 @@ function handleInputAfterGui(ev)
break;
}
break;
case INPUT_PRESELECTEDACTION:
switch (ev.type)
{
@ -874,6 +1062,7 @@ function handleInputAfterGui(ev)
}
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
@ -947,7 +1136,7 @@ function handleInputAfterGui(ev)
}
// TODO: Should we handle "control all units" here as well?
ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank);
ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank, false);
}
else
{
@ -986,35 +1175,57 @@ function handleInputAfterGui(ev)
switch (ev.type)
{
case "mousemotion":
placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementEntity,
"x": placementPosition.x,
"z": placementPosition.z
});
if (snapData)
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (placementSupport.mode === "wall")
{
placementAngle = snapData.angle;
placementPosition.x = snapData.x;
placementPosition.z = snapData.z;
// 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 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();
updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
return false; // continue processing mouse motion
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
dragStart = [ ev.x, ev.y ];
inputState = INPUT_BUILDING_CLICK;
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
resetPlacementEntity();
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
@ -1027,11 +1238,11 @@ function handleInputAfterGui(ev)
switch (ev.hotkey)
{
case "session.rotate.cw":
placementAngle += rotation_step;
placementSupport.angle += rotation_step;
updateBuildingPlacementPreview();
break;
case "session.rotate.ccw":
placementAngle -= rotation_step;
placementSupport.angle -= rotation_step;
updateBuildingPlacementPreview();
break;
}
@ -1054,7 +1265,7 @@ function doAction(action, ev)
switch (action.type)
{
case "move":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
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;
@ -1105,7 +1316,7 @@ function doAction(action, ev)
}
else
{
pos = Engine.GetTerrainAtPoint(ev.x, ev.y);
pos = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
}
Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": pos.x, "z": pos.z, "data": action.data});
// Display rally point at the new coordinates, to avoid display lag
@ -1117,7 +1328,7 @@ function doAction(action, ev)
return true;
case "unset-rallypoint":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
@ -1175,11 +1386,29 @@ function handleMinimapEvent(target)
}
// Called by GUI when user clicks construction button
function startBuildingPlacement(buildEntType)
// @param buildTemplate Template name of the entity the user wants to build
function startBuildingPlacement(buildTemplate)
{
placementEntity = buildEntType;
placementAngle = defaultPlacementAngle;
inputState = INPUT_BUILDING_PLACEMENT;
// 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.SetDefaultAngle();
// find out if we're building a wall, and change the entity appropriately if so
var templateData = GetTemplateData(buildTemplate);
if (templateData.wallSet)
{
placementSupport.mode = "wall";
placementSupport.wallSet = templateData.wallSet;
inputState = INPUT_BUILDING_PLACEMENT;
}
else
{
placementSupport.mode = "building";
placementSupport.template = buildTemplate;
inputState = INPUT_BUILDING_PLACEMENT;
}
}
// Called by GUI when user changes preferred trading goods

View File

@ -0,0 +1,31 @@
function PlacementSupport()
{
this.Reset();
}
PlacementSupport.DEFAULT_ANGLE = Math.PI*3/4;
/**
* Resets the building placement support state. Use this to cancel construction of an entity.
*/
PlacementSupport.prototype.Reset = function()
{
this.mode = null;
this.position = null;
this.template = null;
this.wallSet = null; // maps types of wall pieces ("tower", "long", "short", ...) to template names
this.wallSnapEntities = null; // list of candidate entities to snap the starting and (!) ending positions to when building walls
this.wallEndPosition = null;
this.wallSnapEntitiesIncludeOffscreen = false; // should the next update of the snap candidate list include offscreen towers?
this.wallDragTooltip = null; // tooltip text while the user is draggin the wall. Used to indicate the current cost to build the wall.
this.SetDefaultAngle();
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
Engine.GuiInterfaceCall("SetWallPlacementPreview", {"wallSet": null});
};
PlacementSupport.prototype.SetDefaultAngle = function()
{
this.angle = PlacementSupport.DEFAULT_ANGLE;
};

View File

@ -187,6 +187,10 @@ function getSavedGameData()
}
var lastTickTime = new Date;
/**
* Called every frame.
*/
function onTick()
{
var now = new Date;
@ -205,7 +209,8 @@ function onTick()
updateCursorAndTooltip();
// If the selection changed, we need to regenerate the sim display
// 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)
{
onSimulationUpdate();
@ -292,6 +297,10 @@ function checkPlayerState()
}
}
/**
* 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_Selection.dirty = false;

View File

@ -9,6 +9,7 @@
<script file="gui/common/timer.js"/>
<script file="gui/session/session.js"/>
<script file="gui/session/selection.js"/>
<script file="gui/session/placement.js"/>
<script file="gui/session/input.js"/>
<script file="gui/session/menu.js"/>
<script file="gui/session/selection_details.js"/>
@ -465,10 +466,16 @@
</object>
<!-- ================================ ================================ -->
<!-- Information tooltip -->
<!-- Information tooltip
Follows the mouse around if 'independent' is set to 'true'. -->
<!-- ================================ ================================ -->
<object name="informationTooltip" type="tooltip" independent="true" style="informationTooltip"/>
<!-- ================================ ================================ -->
<!-- Wall-dragging tooltip. Shows the total cost of building a wall while the player is dragging it. -->
<!-- ================================ ================================ -->
<object name="wallDragTooltip" type="tooltip" independent="true" style="informationTooltip"/>
<!-- ================================ ================================ -->
<!-- START of BOTTOM PANEL -->
<!-- ================================ ================================ -->

View File

@ -133,7 +133,18 @@ function setOverlay(object, value)
object.hidden = !value;
}
// Sets up "unit panels" - the panels with rows of icons (Helper function for updateUnitDisplay)
/**
* 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, items, callback)
{
usedPanels[guiName] = 1;
@ -320,7 +331,7 @@ function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
var [batchSize, batchIncrement] = getTrainingBatchStatus(unitEntState.id, entType);
var trainNum = batchSize ? batchSize+batchIncrement : batchIncrement;
tooltip += "\n" + getEntityCost(template);
tooltip += "\n" + getEntityCostTooltip(template);
if (template.health)
tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health;
@ -340,7 +351,7 @@ function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
if (template.tooltip)
tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
tooltip += "\n" + getEntityCost(template);
tooltip += "\n" + getEntityCostTooltip(template);
if (item.pair)
{
@ -348,7 +359,7 @@ function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
if (template1.tooltip)
tooltip1 += "\n[font=\"serif-13\"]" + template1.tooltip + "[/font]";
tooltip1 += "\n" + getEntityCost(template1);
tooltip1 += "\n" + getEntityCostTooltip(template1);
}
break;
@ -357,9 +368,9 @@ function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
if (template.tooltip)
tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
tooltip += "\n" + getEntityCost(template);
tooltip += "\n" + getEntityCostTooltip(template); // see utility_functions.js
tooltip += getPopulationBonusTooltip(template); // see utility_functions.js
tooltip += getPopulationBonus(template);
if (template.health)
tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health;
@ -685,7 +696,15 @@ function setupUnitBarterPanel(unitEntState)
}
}
// Updates right Unit Commands Panel - runs in the main session loop via updateSelectionDetails()
/**
* 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 selection Array of currently selected entity IDs.
*/
function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection)
{
// Panels that are active

View File

@ -3,6 +3,14 @@ const FLORA = "flora";
const FAUNA = "fauna";
const SPECIAL = "special";
const COST_DISPLAY_NAMES = {
"food": "Food",
"wood": "Wood",
"stone": "Stone",
"metal": "Metal",
"population": "Population",
};
//-------------------------------- -------------------------------- --------------------------------
// Utility functions
//-------------------------------- -------------------------------- --------------------------------
@ -169,27 +177,74 @@ function getEntityCommandsList(entState)
return commands;
}
function getEntityCost(template)
/**
* Translates a cost component identifier as they are used internally (e.g. "population", "food", etc.) to proper
* display names.
*/
function getCostComponentDisplayName(costComponentName)
{
var cost = "";
if (template.cost)
{
var costs = [];
if (template.cost.food) costs.push(template.cost.food + " [font=\"serif-12\"]Food[/font]");
if (template.cost.wood) costs.push(template.cost.wood + " [font=\"serif-12\"]Wood[/font]");
if (template.cost.metal) costs.push(template.cost.metal + " [font=\"serif-12\"]Metal[/font]");
if (template.cost.stone) costs.push(template.cost.stone + " [font=\"serif-12\"]Stone[/font]");
if (template.cost.population) costs.push(template.cost.population + " [font=\"serif-12\"]Population[/font]");
return COST_DISPLAY_NAMES[costComponentName];
}
cost += "[font=\"serif-bold-13\"]Costs:[/font] " + costs.join(", ");
/**
* Helper function for getEntityCostTooltip.
*/
function getEntityCostComponentsTooltipString(template)
{
var costs = [];
if (template.cost.food) costs.push(template.cost.food + " [font=\"serif-12\"]" + getCostComponentDisplayName("food") + "[/font]");
if (template.cost.wood) costs.push(template.cost.wood + " [font=\"serif-12\"]" + getCostComponentDisplayName("wood") + "[/font]");
if (template.cost.metal) costs.push(template.cost.metal + " [font=\"serif-12\"]" + getCostComponentDisplayName("metal") + "[/font]");
if (template.cost.stone) costs.push(template.cost.stone + " [font=\"serif-12\"]" + getCostComponentDisplayName("stone") + "[/font]");
if (template.cost.population) costs.push(template.cost.population + " [font=\"serif-12\"]" + getCostComponentDisplayName("population") + "[/font]");
return costs;
}
/**
* Returns the cost information to display in the specified entity's construction button tooltip.
*/
function getEntityCostTooltip(template)
{
var cost = "[font=\"serif-bold-13\"]Costs:[/font] ";
// 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);
// TODO: the costs of the wall segments should be the same, and for now we will assume they are (ideally we
// should take the average here or something).
var wallCosts = getEntityCostComponentsTooltipString(templateLong);
var towerCosts = getEntityCostComponentsTooltipString(templateTower);
cost += "\n";
cost += " Walls: " + wallCosts.join(", ") + "\n";
cost += " Towers: " + towerCosts.join(", ");
}
else if (template.cost)
{
var costs = getEntityCostComponentsTooltipString(template);
cost += costs.join(", ");
}
else
{
cost = ""; // cleaner than duplicating the serif-bold-13 stuff
}
return cost;
}
function getPopulationBonus(template)
/**
* Returns the population bonus information to display in the specified entity's construction button tooltip.
*/
function getPopulationBonusTooltip(template)
{
var popBonus = "";
if (template.cost.populationBonus)
if (template.cost && template.cost.populationBonus)
popBonus = "\n[font=\"serif-bold-13\"]Population Bonus:[/font] " + template.cost.populationBonus;
return popBonus;
}

View File

@ -71,6 +71,9 @@ BuildRestrictions.prototype.Init = function()
this.territories = this.template.Territory.split(/\s+/);
};
/**
* Returns true iff this entity can be built at its current position.
*/
BuildRestrictions.prototype.CheckPlacement = function(player)
{
// TODO: Return error code for invalid placement, which can be handled by the UI

View File

@ -189,15 +189,38 @@ Foundation.prototype.Build = function(builderEnt, work)
cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
// TODO: should add a ICmpPosition::CopyFrom() instead of all this
// ----------------------------------------------------------------------
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
// ----------------------------------------------------------------------
// Copy over the obstruction control group IDs from the foundation entities. This is needed to ensure that when a foundation
// is completed and replaced by a new entity, it remains in the same control group(s) as any other foundation entities that
// may surround it. This is the mechanism that is used to e.g. enable wall pieces to be built closely together, ignoring their
// mutual obstruction shapes (since they would otherwise be prevented from being built so closely together). If the control
// groups are not copied over, the new entity will default to a new control group containing only itself, and will hence block
// construction of any surrounding foundations that it was previously in the same control group with.
// Note that this will result in the completed building entities having control group IDs that equal entity IDs of old (and soon
// to be deleted) foundation entities. This should not have any consequences, however, since the control group IDs are only meant
// to be unique identifiers, which is still true when reusing the old ones.
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
// ----------------------------------------------------------------------
var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter();
var cmpIdentity = Engine.QueryInterface(building, IID_Identity);
if (cmpIdentity.GetClassesList().indexOf("CivCentre") != -1) cmpPlayerStatisticsTracker.IncreaseBuiltCivCentresCounter();
if (cmpIdentity.GetClassesList().indexOf("CivCentre") != -1)
cmpPlayerStatisticsTracker.IncreaseBuiltCivCentresCounter();
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
@ -207,7 +230,6 @@ Foundation.prototype.Build = function(builderEnt, work)
Engine.PostMessage(this.entity, MT_ConstructionFinished,
{ "entity": this.entity, "newentity": building });
Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: building });
Engine.DestroyEntity(this.entity);

View File

@ -18,6 +18,8 @@ GuiInterface.prototype.Deserialize = function(obj)
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.rallyPoints = undefined;
this.notifications = [];
this.renamedEntities = [];
@ -141,7 +143,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
var ret = {
"id": ent,
"template": template
}
};
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
@ -214,6 +216,15 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
};
}
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
{
ret.obstruction = {
"controlGroup": cmpObstruction.GetControlGroup(),
"controlGroup2": cmpObstruction.GetControlGroup2(),
};
}
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
{
@ -331,6 +342,26 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
}
}
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.Cost)
{
ret.cost = {};
@ -342,6 +373,44 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus;
}
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
{
ret.obstruction.shape.type = "unit";
ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"];
}
}
if (template.Health)
{
ret.health = +template.Health.Max;
@ -367,6 +436,26 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed;
}
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;
};
@ -605,6 +694,7 @@ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
* 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 true if the placement is okay (everything is valid and the entity is not obstructed by others).
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
@ -673,11 +763,571 @@ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
return false;
};
/**
* 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},
};
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;
// 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;
}
validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player));
}
allPiecesValid = allPiecesValid && validPlacement;
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid)
cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColour(1, 1, 1, 1);
}
// 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;
}
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 (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")
{
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
@ -914,6 +1564,7 @@ var exposedFunctions = {
"SetStatusBars": 1,
"DisplayRallyPoint": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnit": 1,

View File

@ -0,0 +1,16 @@
function WallPiece() {}
WallPiece.prototype.Schema =
"<a:help></a:help>" +
"<a:example>" +
"</a:example>" +
"<element name='Length'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>";
WallPiece.prototype.Init = function()
{
};
Engine.RegisterComponentType(IID_WallPiece, "WallPiece", WallPiece);

View File

@ -0,0 +1,40 @@
function WallSet() {}
WallSet.prototype.Schema =
"<a:help></a:help>" +
"<a:example>" +
"</a:example>" +
"<element name='Templates'>" +
"<interleave>" +
"<element name='Tower' a:help='Template name of the tower piece'>" +
"<text/>" +
"</element>" +
"<element name='Gate' a:help='Template name of the gate piece'>" +
"<text/>" +
"</element>" +
"<element name='WallLong' a:help='Template name of the long wall segment'>" +
"<text/>" +
"</element>" +
"<element name='WallMedium' a:help='Template name of the medium-size wall segment'>" +
"<text/>" +
"</element>" +
"<element name='WallShort' a:help='Template name of the short wall segment'>" +
"<text/>" +
"</element>" +
"</interleave>" +
"</element>" +
"<element name='MinTowerOverlap' a:help='Maximum fraction that wall segments are allowed to overlap towers, where 0 signifies no overlap and 1 full overlap'>" +
"<data type='decimal'><param name='minInclusive'>0.0</param><param name='maxInclusive'>1.0</param></data>" +
"</element>" +
"<element name='MaxTowerOverlap' a:help='Minimum fraction that wall segments are required to overlap towers, where 0 signifies no overlap and 1 full overlap'>" +
"<data type='decimal'><param name='minInclusive'>0.0</param><param name='maxInclusive'>1.0</param></data>" +
"</element>";
WallSet.prototype.Init = function()
{
};
WallSet.prototype.Serialize = null;
Engine.RegisterComponentType(IID_WallSet, "WallSet", WallSet);

View File

@ -0,0 +1 @@
Engine.RegisterInterface("WallPiece");

View File

@ -0,0 +1 @@
Engine.RegisterInterface("WallSet");

View File

@ -8,12 +8,15 @@ function ProcessCommand(player, cmd)
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerMan || player < 0)
return;
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
if (playerEnt == INVALID_ENTITY)
return;
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
if (!cmpPlayer)
return;
var controlAllUnits = cmpPlayer.CanControlAllUnits();
// Note: checks of UnitAI targets are not robust enough here, as ownership
@ -203,151 +206,11 @@ function ProcessCommand(player, cmd)
break;
case "construct":
// Message structure:
// {
// "type": "construct",
// "entities": [...],
// "template": "...",
// "x": ...,
// "z": ...,
// "angle": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true,
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check that we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
break;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity("foundation|" + cmd.template);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
break;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(cmd.angle);
// Check whether it's obstructed by other entities or invalid terrain
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
{
if (g_DebugCommands)
{
warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" });
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
break;
}
// Check build limits
var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
{
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
}
// TODO: The UI should tell the user they can't build this (but we still need this check)
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
break;
}
var cmpTechMan = QueryPlayerIDInterface(player, IID_TechnologyManager);
// TODO: Enable this check once the AI gets technology support
if (!cmpTechMan.CanProduce(cmd.template) && false)
{
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);
}
// TODO: AI has no visibility info
if (!cmpPlayer.IsAI())
{
// Check whether it's in a visible or fogged region
// tell GetLosVisibility to force RetainInFog because preview entities set this to false,
// which would show them as hidden instead of fogged
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
if (!visible)
{
if (g_DebugCommands)
{
warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" });
Engine.DestroyEntity(ent);
break;
}
}
var cmpCost = Engine.QueryInterface(ent, IID_Cost);
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
if (g_DebugCommands)
{
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
}
Engine.DestroyEntity(ent);
break;
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued
});
}
TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd);
break;
case "construct-wall":
TryConstructWall(player, cmpPlayer, controlAllUnits, cmd);
break;
case "delete-entities":
@ -549,6 +412,420 @@ function ExtractFormations(ents)
return { "entities": entities, "members": members, "ids": ids };
}
/**
* 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": ...,
// "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;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity("foundation|" + cmd.template);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
return false;
}
// 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);
}
// Check whether it's obstructed by other entities or invalid terrain
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
{
if (g_DebugCommands)
{
warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" });
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
return false;
}
// Check build limits
var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
{
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
}
// TODO: The UI should tell the user they can't build this (but we still need this check)
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
return false;
}
var cmpTechMan = QueryPlayerIDInterface(player, IID_TechnologyManager);
// TODO: Enable this check once the AI gets technology support
if (!cmpTechMan.CanProduce(cmd.template) && false)
{
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);
}
// TODO: AI has no visibility info
if (!cmpPlayer.IsAI())
{
// Check whether it's in a visible or fogged region
// tell GetLosVisibility to force RetainInFog because preview entities set this to false,
// which would show them as hidden instead of fogged
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
if (!visible)
{
if (g_DebugCommands)
{
warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" });
Engine.DestroyEntity(ent);
return false;
}
}
var cmpCost = Engine.QueryInterface(ent, IID_Cost);
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
if (g_DebugCommands)
{
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
}
Engine.DestroyEntity(ent);
return false;
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued
});
}
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];
// '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.
*/

View File

@ -0,0 +1,203 @@
/**
* Returns the wall piece entities needed to construct a wall between start.pos and end.pos. Assumes start.pos != end.pos.
* The result is an array of objects, each one containing the following information about a single wall piece entity:
* - 'template': the template name of the entity
* - 'pos': position of the entity, as an object with keys 'x' and 'z'
* - 'angle': orientation of the entity, as an angle in radians
*
* All the pieces in the resulting array are ordered left-to-right (or right-to-left) as they appear in the physical wall.
*
* @param placementData Object that associates the wall piece template names with information about those kinds of pieces.
* Expects placementData[templateName].templateData to contain the parsed template information about
* the template whose filename is <i>templateName</i>.
* @param wallSet Object that primarily holds the template names for the supported wall pieces in this set (under the
* 'templates' key), as well as the min and max allowed overlap factors (see GetWallSegmentsRec). Expected
* to contain template names for keys "long" (long wall segment), "medium" (medium wall segment), "short"
* (short wall segment), "tower" (intermediate tower between wall segments), "gate" (replacement for long
* walls).
* @param start Object holding the starting position of the wall. Must contain keys 'x' and 'z'.
* @param end Object holding the ending position of the wall. Must contains keys 'x' and 'z'.
*/
function GetWallPlacement(placementData, wallSet, start, end)
{
var result = [];
var candidateSegments = [
{"template": wallSet.templates.long, "len": placementData[wallSet.templates.long].templateData.wallPiece.length},
{"template": wallSet.templates.medium, "len": placementData[wallSet.templates.medium].templateData.wallPiece.length},
{"template": wallSet.templates.short, "len": placementData[wallSet.templates.short].templateData.wallPiece.length},
];
var towerWidth = placementData[wallSet.templates.tower].templateData.wallPiece.length;
var dir = {"x": end.pos.x - start.pos.x, "z": end.pos.z - start.pos.z};
var len = Math.sqrt(dir.x * dir.x + dir.z * dir.z);
// we'll need room for at least our starting and ending towers to fit next to eachother
if (len <= towerWidth)
return result;
var placement = GetWallSegmentsRec(len, candidateSegments, wallSet.minTowerOverlap, wallSet.maxTowerOverlap, towerWidth, 0, []);
// TODO: make sure intermediate towers are spaced out far enough for their obstructions to not overlap, implying that
// tower's wallpiece lengths should be > their obstruction width, which is undesirable because it prevents towers with
// wide bases
if (placement)
{
var placedEntities = placement.segments; // list of chosen candidate segments
var r = placement.r; // remaining distance to target without towers (must be <= (N-1) * towerWidth)
var s = r / (2 * placedEntities.length); // spacing
var dirNormalized = {"x": dir.x / len, "z": dir.z / len};
var angle = -Math.atan2(dir.z, dir.x); // angle of this wall segment (relative to world-space X/Z axes)
var progress = 0;
for (var i = 0; i < placedEntities.length; i++)
{
var placedEntity = placedEntities[i];
var targetX = start.pos.x + (progress + s + placedEntity.len/2) * dirNormalized.x;
var targetZ = start.pos.z + (progress + s + placedEntity.len/2) * dirNormalized.z;
result.push({
"template": placedEntity.template,
"pos": {"x": targetX, "z": targetZ},
"angle": angle,
});
if (i < placedEntities.length - 1)
{
var towerX = start.pos.x + (progress + placedEntity.len + 2*s) * dirNormalized.x;
var towerZ = start.pos.z + (progress + placedEntity.len + 2*s) * dirNormalized.z;
result.push({
"template": wallSet.templates.tower,
"pos": {"x": towerX, "z": towerZ},
"angle": angle,
});
}
progress += placedEntity.len + 2*s;
}
}
else
{
error("No placement possible for distance=" + Math.round(len*1000)/1000.0 + ", minOverlap=" + wallSet.minTowerOverlap + ", maxOverlap=" + wallSet.maxTowerOverlap);
}
return result;
}
/**
* Helper function for GetWallPlacement. Finds a list of wall segments and the corresponding remaining spacing/overlap
* distance "r" that will suffice to construct a wall of the given distance. It is understood that two extra towers will
* be placed centered at the starting and ending points of the wall.
*
* @param d Total distance between starting and ending points (constant throughout calls).
* @param candidateSegments List of candidate segments (constant throughout calls). Should be ordered longer-to-shorter
* for better execution speed.
* @param minOverlap Minimum overlap factor (constant throughout calls). Must have a value between 0 (meaning walls are
* not allowed to overlap towers) and 1 (meaning they're allowed to overlap towers entirely).
* Must be <= maxOverlap.
* @param maxOverlap Maximum overlap factor (constant throughout calls). Must have a value between 0 (meaning walls are
* not allowed to overlap towers) and 1 (meaning they're allowed to overlap towers entirely).
* Must be >= minOverlap.
* @param t Length of a single tower (constant throughout calls). Acts as buffer space for wall segments (see comments).
* @param distSoFar Sum of all the wall segments' lengths in 'segments'.
* @param segments Current list of wall segments placed.
*/
function GetWallSegmentsRec(d, candidateSegments, minOverlap, maxOverlap, t, distSoFar, segments)
{
// The idea is to find a number N of wall segments (excluding towers) so that the sum of their lengths adds up to a
// value that is within certain bounds of the distance 'd' between the starting and ending points of the wall. This
// creates either a positive or negative 'buffer' of space, that can be compensated for by spacing the wall segments
// out away from each other, or inwards, overlapping each other. The spaces or overlaps can then be covered up by
// placing towers on top of them. In this way, the same set of wall segments can be used to span a wider range of
// target distances.
//
// In this function, it is understood that two extra towers will be placed centered at the starting and ending points.
// They are allowed to contribute to the buffer space.
//
// The buffer space equals the difference between d and the sum of the lengths of all the wall segments, and is denoted
// 'r' for 'remaining space'. Positive values of r mean that the walls will need to be spaced out, negative values of r
// mean that they will need to overlap. Clearly, there are limits to how far wall segments can be spaced out or
// overlapped, depending on how much 'buffer space' each tower provides, and how far 'into' towers the wall segments are
// allowed to overlap.
//
// Let 't' signify the width of a tower. When there are N wall segments, then the maximum distance that can be covered
// using only these walls (plus the towers covering up any gaps) is achieved when the walls and towers touch outer-border-
// to-outer-border. Therefore, the maximum value of r is then given by:
//
// rMax = t/2 + (N-1)*t + t/2
// = N*t
//
// where the two half-tower widths are buffer space contributed by the implied towers on the starting and ending points.
// Similarly, a value rMin = -N*t can be derived for the minimal value of r. Note that a value of r = 0 means that the
// wall segment lengths add up to exactly d, meaning that each one starts and ends right in the center of a tower.
//
// Thus, we establish:
// -Nt <= r <= Nt
//
// We can further generalize this by adding in parameters to control the depth to within which wall segments are allowed to
// overlap with a tower. The bounds above assume that a wall segment is allowed to overlap across the entire range of 0
// (not overlapping at all, as in the upper boundary) to 1 (overlapping maximally, as in the lower boundary).
//
// By requiring that walls overlap towers to a degree of at least 0 < minOverlap <= 1, it is clear that this lowers the
// distance that can be maximally reached by the same set of wall segments, compared to the value of minOverlap = 0 that
// we assumed to initially find Nt.
//
// Consider a value of minOverlap = 0.5, meaning that any wall segment must protrude at least halfway into towers; in this
// situation, wall segments must at least touch boundaries or overlap mutually, implying that the sum of their lengths
// must equal or exceed 'd', establishing an upper bound of 0 for r.
// Similarly, consider a value of minOverlap = 1, meaning that any wall segment must overlap towers maximally; this situation
// is equivalent to the one for finding the lower bound -Nt on r.
//
// With the implicit value minOverlap = 0 that yielded the upper bound Nt above, simple interpolation and a similar exercise
// for maxOverlap, we find:
// (1-2*maxOverlap) * Nt <= r <= (1-2*minOverlap) * Nt
//
// To find N segments that satisfy this requirement, we try placing L, M and S wall segments in turn and continue recursively
// as long as the value of r is not within the bounds. If continuing recursively returns an impossible configuration, we
// backtrack and try a wall segment of the next length instead. Note that we should prefer to use the long segments first since
// they can be replaced by gates.
for each (var candSegment in candidateSegments)
{
segments.push(candSegment);
var newDistSoFar = distSoFar + candSegment.len;
var r = d - newDistSoFar;
var rLowerBound = (1 - 2 * maxOverlap) * segments.length * t;
var rUpperBound = (1 - 2 * minOverlap) * segments.length * t;
if (r < rLowerBound)
{
// we've allocated too much wall length, pop the last segment and try the next
//warn("Distance so far exceeds target, trying next level");
segments.pop();
continue;
}
else if (r > rUpperBound)
{
var recursiveResult = GetWallSegmentsRec(d, candidateSegments, minOverlap, maxOverlap, t, newDistSoFar, segments);
if (!recursiveResult)
{
// recursive search with this piece yielded no results, pop it and try the next one
segments.pop();
continue;
}
else
return recursiveResult;
}
else
{
// found a placement
return {"segments": segments, "r": r};
}
}
// no placement possible :(
return false;
}
Engine.RegisterGlobal("GetWallPlacement", GetWallPlacement);

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>props/special/palisade_rocks_gate.xml</Actor>
</VisualActor>
<WallPiece>
<Length>11.0</Length>
</WallPiece>
</Entity>

View File

@ -22,4 +22,7 @@
<VisualActor>
<Actor>props/special/palisade_rocks_long.xml</Actor>
</VisualActor>
<WallPiece>
<Length>11.0</Length>
</WallPiece>
</Entity>

View File

@ -22,4 +22,7 @@
<VisualActor>
<Actor>props/special/palisade_rocks_medium.xml</Actor>
</VisualActor>
<WallPiece>
<Length>8.0</Length>
</WallPiece>
</Entity>

View File

@ -22,5 +22,7 @@
<VisualActor>
<Actor>props/special/palisade_rocks_short.xml</Actor>
</VisualActor>
<WallPiece>
<Length>4.0</Length>
</WallPiece>
</Entity>

View File

@ -22,4 +22,7 @@
<VisualActor>
<Actor>props/special/palisade_rocks_tower.xml</Actor>
</VisualActor>
<WallPiece>
<Length>4.0</Length>
</WallPiece>
</Entity>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_structure_defense_wallset">
<Identity>
<Civ>gaia</Civ>
<SpecificName>Palisade</SpecificName>
<History>A cheap, quick defensive structure constructed with sharpened tree trunks</History>
<Icon>gaia/special_palisade.png</Icon>
</Identity>
<WallSet>
<Templates>
<Tower>other/palisades_rocks_tower</Tower>
<Gate>other/palisades_rocks_gate</Gate>
<WallLong>other/palisades_rocks_long</WallLong>
<WallMedium>other/palisades_rocks_medium</WallMedium>
<WallShort>other/palisades_rocks_short</WallShort>
</Templates>
</WallSet>
</Entity>

View File

@ -17,4 +17,7 @@
<VisualActor>
<Actor>structures/carthaginians/wall_gate.xml</Actor>
</VisualActor>
<WallPiece>
<Length>3.0</Length>
</WallPiece>
</Entity>

View File

@ -24,4 +24,7 @@
<VisualActor>
<Actor>structures/carthaginians/wall_long.xml</Actor>
</VisualActor>
<WallPiece>
<Length>46.0</Length>
</WallPiece>
</Entity>

View File

@ -24,4 +24,7 @@
<VisualActor>
<Actor>structures/carthaginians/wall_medium.xml</Actor>
</VisualActor>
<WallPiece>
<Length>30.0</Length>
</WallPiece>
</Entity>

View File

@ -24,4 +24,7 @@
<VisualActor>
<Actor>structures/carthaginians/wall_short.xml</Actor>
</VisualActor>
<WallPiece>
<Length>16.0</Length>
</WallPiece>
</Entity>

View File

@ -25,4 +25,7 @@
<Actor>structures/carthaginians/wall_tower.xml</Actor>
<FoundationActor>structures/fndn_3x3.xml</FoundationActor>
</VisualActor>
<WallPiece>
<Length>11.0</Length>
</WallPiece>
</Entity>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_structure_defense_wallset">
<Identity>
<Civ>cart</Civ>
<SpecificName>Jdar</SpecificName>
<History>The Carthaginians built what are referred to as "triple walls" to fortify some of their cities; as triple walls aren't a practical construct for 0 A.D, the construction of the inner wall is to be used. This wall served not only as a defensive structure but had barracks and stables integrated right into it, and raised towers at intervals. Fodder for elephants and horses, and arms were stored onsite. The ground level consisted of housing for elephants, the second level for horses, and the third level as barracks for the troops. In Carthage alone, 200 elephants, a thousand horses and 15,000~30,000 troops could be housed within the city walls. As shown in the reference drawing, there was also a ditch at the base in front of the wall. These walls were typically built of large blocks of sandstone hewn from deposits nearby, and were never breached by invaders.</History>
</Identity>
<WallSet>
<Templates>
<Tower>structures/cart_wall_tower</Tower>
<Gate>structures/cart_wall_gate</Gate>
<WallLong>structures/cart_wall_long</WallLong>
<WallMedium>structures/cart_wall_medium</WallMedium>
<WallShort>structures/cart_wall_short</WallShort>
</Templates>
</WallSet>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/celts/wall_gate.xml</Actor>
</VisualActor>
<WallPiece>
<Length>25.0</Length>
</WallPiece>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/celts/wall_long.xml</Actor>
</VisualActor>
<WallPiece>
<Length>37.0</Length>
</WallPiece>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/celts/wall_medium.xml</Actor>
</VisualActor>
<WallPiece>
<Length>25.0</Length>
</WallPiece>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/celts/wall_short.xml</Actor>
</VisualActor>
<WallPiece>
<Length>13.0</Length>
</WallPiece>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/celts/wall_tower.xml</Actor>
</VisualActor>
<WallPiece>
<Length>9.0</Length>
</WallPiece>
</Entity>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_structure_defense_wallset">
<Identity>
<Civ>celt</Civ>
<SpecificName>Gwarchglawdd</SpecificName>
<History>The Romans called this wall 'Murus Gallicus'. Translated, it means 'Gaulish wall'. It was extremely resistant to assault by battering ram. Julius Caesar described a type of wood and stone wall, known as a Murus Gallicus, in his account of the Gallic Wars. These walls were made of a stone wall filled with rubble, with wooden logs inside for stability. Caesar noted how the flexibility of the wood added to the strength of the fort in case of battering ram attack.</History>
</Identity>
<WallSet>
<Templates>
<Tower>structures/celt_wall_tower</Tower>
<Gate>structures/celt_wall_gate</Gate>
<WallLong>structures/celt_wall_long</WallLong>
<WallMedium>structures/celt_wall_medium</WallMedium>
<WallShort>structures/celt_wall_short</WallShort>
</Templates>
<MaxTowerOverlap>0.80</MaxTowerOverlap>
<MinTowerOverlap>0.05</MinTowerOverlap>
</WallSet>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/hellenes/wall_gate.xml</Actor>
</VisualActor>
<WallPiece>
<Length>38.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/hellenes/wall_long.xml</Actor>
</VisualActor>
<WallPiece>
<Length>37.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/hellenes/wall_medium.xml</Actor>
</VisualActor>
<WallPiece>
<Length>24.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/hellenes/wall_medium.xml</Actor>
</VisualActor>
<WallPiece>
<Length>24.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/hellenes/wall_short.xml</Actor>
</VisualActor>
<WallPiece>
<Length>13.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/hellenes/wall_tower.xml</Actor>
</VisualActor>
<WallPiece>
<Length>7.5</Length>
</WallPiece>
</Entity>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_structure_defense_wallset">
<Identity>
<Civ>hele</Civ>
<SpecificName>Teîkhos</SpecificName>
<History>All Hellenic cities were surrounded by stone walls for protection against enemy raids. Some of these fortifications, like the Athenian Long Walls, for example, were massive structures.</History>
</Identity>
<WallSet>
<Templates>
<Tower>structures/hele_wall_tower</Tower>
<Gate>structures/hele_wall_gate</Gate>
<WallLong>structures/hele_wall_long</WallLong>
<WallMedium>structures/hele_wall_med</WallMedium>
<WallShort>structures/hele_wall_short</WallShort>
</Templates>
<MaxTowerOverlap>0.90</MaxTowerOverlap>
<MinTowerOverlap>0.05</MinTowerOverlap>
</WallSet>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/iberians/wall_gate.xml</Actor>
</VisualActor>
<WallPiece>
<Length>36.0</Length>
</WallPiece>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/iberians/wall_long.xml</Actor>
</VisualActor>
<WallPiece>
<Length>36.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/iberians/wall_medium.xml</Actor>
</VisualActor>
<WallPiece>
<Length>24.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/iberians/wall_short.xml</Actor>
</VisualActor>
<WallPiece>
<Length>12.0</Length>
</WallPiece>
</Entity>

View File

@ -10,9 +10,12 @@
<History>Sturdy battlements for city walls.</History>
</Identity>
<Obstruction>
<Static width="11" depth="11"/>
<Static width="10" depth="10"/>
</Obstruction>
<VisualActor>
<Actor>structures/iberians/wall_tower.xml</Actor>
</VisualActor>
<WallPiece>
<Length>10</Length>
</WallPiece>
</Entity>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_structure_defense_wallset">
<Identity>
<Civ>iber</Civ>
<SpecificName>Muro Ancho</SpecificName>
<History>High and strongly built defensive stone walls were a common structure of the Iberian Peninsula during the period, and for long thereafter.</History>
</Identity>
<WallSet>
<Templates>
<Tower>structures/iber_wall_tower</Tower>
<Gate>structures/iber_wall_gate</Gate>
<WallLong>structures/iber_wall_long</WallLong>
<WallMedium>structures/iber_wall_medium</WallMedium>
<WallShort>structures/iber_wall_short</WallShort>
</Templates>
<MaxTowerOverlap>0.80</MaxTowerOverlap>
<MinTowerOverlap>0.20</MinTowerOverlap>
</WallSet>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/persians/wall_gate.xml</Actor>
</VisualActor>
<WallPiece>
<Length>37.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/persians/wall_long.xml</Actor>
</VisualActor>
<WallPiece>
<Length>37.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/persians/wall_medium.xml</Actor>
</VisualActor>
<WallPiece>
<Length>24.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/persians/wall_short.xml</Actor>
</VisualActor>
<WallPiece>
<Length>13.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/persians/wall_tower.xml</Actor>
</VisualActor>
<WallPiece>
<Length>8.5</Length>
</WallPiece>
</Entity>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_structure_defense_wallset">
<Identity>
<Civ>pers</Civ>
<SpecificName>Dida</SpecificName>
<History>These were the massive walls that Nebuchadnezzar built to protect the city. It is said that two four-horse chariots could easily pass by each other. Babylon, although not an official royal residence (there were 4 of them all together), was a preferred place for holidays.</History>
</Identity>
<WallSet>
<Templates>
<Tower>structures/pers_wall_tower</Tower>
<Gate>structures/pers_wall_gate</Gate>
<WallLong>structures/pers_wall_long</WallLong>
<WallMedium>structures/pers_wall_medium</WallMedium>
<WallShort>structures/pers_wall_short</WallShort>
</Templates>
</WallSet>
</Entity>

View File

@ -24,10 +24,10 @@
</Health>
<Identity>
<Civ>rome</Civ>
<GenericName>Siege Wall</GenericName>
<GenericName>Siege Wall</GenericName>
<SpecificName>Murus Latericius</SpecificName>
<Icon>structures/palisade.png</Icon>
<Tooltip>A wooden and turf palisade buildable in enemy and neutral territories.</Tooltip>
<Tooltip>A wooden and turf palisade buildable in enemy and neutral territories.</Tooltip>
<History>Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia.</History>
<RequiredTechnology>phase_city</RequiredTechnology>
</Identity>

View File

@ -37,4 +37,7 @@
<Actor>structures/romans/siege_wall_gate.xml</Actor>
<FoundationActor>structures/fndn_wall.xml</FoundationActor>
</VisualActor>
<WallPiece>
<Length>36.0</Length>
</WallPiece>
</Entity>

View File

@ -40,4 +40,7 @@
<Actor>structures/romans/siege_wall_long.xml</Actor>
<FoundationActor>structures/fndn_wall.xml</FoundationActor>
</VisualActor>
<WallPiece>
<Length>36.0</Length>
</WallPiece>
</Entity>

View File

@ -40,4 +40,7 @@
<Actor>structures/romans/siege_wall_medium.xml</Actor>
<FoundationActor>structures/fndn_wall_short.xml</FoundationActor>
</VisualActor>
<WallPiece>
<Length>24.0</Length>
</WallPiece>
</Entity>

View File

@ -40,4 +40,7 @@
<Actor>structures/romans/siege_wall_short.xml</Actor>
<FoundationActor>structures/fndn_1x1.xml</FoundationActor>
</VisualActor>
<WallPiece>
<Length>12.0</Length>
</WallPiece>
</Entity>

View File

@ -37,4 +37,7 @@
<Actor>structures/romans/siege_wall_tower.xml</Actor>
<FoundationActor>structures/fndn_1x1.xml</FoundationActor>
</VisualActor>
<WallPiece>
<Length>6.0</Length>
</WallPiece>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/romans/wall_gate.xml</Actor>
</VisualActor>
<WallPiece>
<Length>37.0</Length>
</WallPiece>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/romans/wall_long.xml</Actor>
</VisualActor>
<WallPiece>
<Length>37.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/romans/wall_medium.xml</Actor>
</VisualActor>
<WallPiece>
<Length>25.0</Length>
</WallPiece>
</Entity>

View File

@ -21,4 +21,7 @@
<VisualActor>
<Actor>structures/romans/wall_short.xml</Actor>
</VisualActor>
<WallPiece>
<Length>13.0</Length>
</WallPiece>
</Entity>

View File

@ -15,4 +15,7 @@
<VisualActor>
<Actor>structures/romans/wall_tower.xml</Actor>
</VisualActor>
<WallPiece>
<Length>9.5</Length>
</WallPiece>
</Entity>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_structure_defense_wallset">
<Identity>
<Civ>rome</Civ>
<GenericName>Siege Wall</GenericName>
<SpecificName>Murus Latericius</SpecificName>
<Icon>structures/palisade.png</Icon>
<Tooltip>A wooden and turf palisade buildable in enemy and neutral territories.</Tooltip>
<History>Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia.</History>
</Identity>
<WallSet>
<Templates>
<Tower>structures/rome_siege_wall_tower</Tower>
<Gate>structures/rome_siege_wall_gate</Gate>
<WallLong>structures/rome_siege_wall_long</WallLong>
<WallMedium>structures/rome_siege_wall_medium</WallMedium>
<WallShort>structures/rome_siege_wall_short</WallShort>
</Templates>
<MaxTowerOverlap>1.00</MaxTowerOverlap>
<MinTowerOverlap>0.05</MinTowerOverlap>
</WallSet>
</Entity>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_structure_defense_wallset">
<Identity>
<Civ>rome</Civ>
<SpecificName>Moenia</SpecificName>
<History>Roman city walls used a number of innovations to thwart besiegers.</History>
</Identity>
<WallSet>
<Templates>
<Tower>structures/rome_wall_tower</Tower>
<Gate>structures/rome_wall_gate</Gate>
<WallLong>structures/rome_wall_long</WallLong>
<WallMedium>structures/rome_wall_medium</WallMedium>
<WallShort>structures/rome_wall_short</WallShort>
</Templates>
<MaxTowerOverlap>0.80</MaxTowerOverlap>
<MinTowerOverlap>0.10</MinTowerOverlap>
</WallSet>
</Entity>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Abstract entity to serve as a bare-minimum dummy constructable entity to initiate wall placement.
Defines the set of actual entities that are part of the same wall construction system (i.e., towers,
gates, wall segments of various length, etc.) using the WallSet component (to be overridden by child
templates). -->
<Entity>
<Identity>
<Icon>structures/wall.png</Icon>
<Classes datatype="tokens">Town Wall</Classes>
<GenericName>City Wall</GenericName>
<Tooltip>Wall off your town for a stout defense.</Tooltip>
<Icon>structures/wall.png</Icon>
<RequiredTechnology>phase_town</RequiredTechnology>
</Identity>
<WallSet>
<MaxTowerOverlap>0.85</MaxTowerOverlap>
<MinTowerOverlap>0.05</MinTowerOverlap>
</WallSet>
</Entity>

View File

@ -20,9 +20,7 @@
structures/{civ}_dock
structures/{civ}_outpost
structures/{civ}_defense_tower
structures/{civ}_wall
structures/{civ}_wall_tower
structures/{civ}_wall_gate
structures/{civ}_wallset_stone
structures/{civ}_fortress
</Entities>
</Builder>

View File

@ -19,9 +19,7 @@
structures/{civ}_barracks
structures/{civ}_dock
structures/{civ}_defense_tower
structures/{civ}_wall
structures/{civ}_wall_tower
structures/{civ}_wall_gate
special/wallsets/{civ}_land
structures/{civ}_fortress
</Entities>
</Builder>

View File

@ -10,6 +10,7 @@
structures/athen_gymnasion
structures/athen_theatron
structures/athen_prytaneion
-structures/{civ}_wallset_stone
</Entities>
</Builder>
<Cost>

View File

@ -5,6 +5,7 @@
structures/athen_gymnasion
structures/athen_theatron
structures/athen_prytaneion
-structures/{civ}_wallset_stone
</Entities>
</Builder>
<Identity>

View File

@ -16,6 +16,7 @@
structures/athen_gymnasion
structures/athen_theatron
structures/athen_prytaneion
-structures/{civ}_wallset_stone
</Entities>
</Builder>
<Cost>

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_unit_infantry_ranged_archer">
<Builder>
<Entities datatype="tokens">
-structures/{civ}_wallset_stone
</Entities>
</Builder>
<Cost>
<Resources>
<food>0</food>

View File

@ -5,6 +5,11 @@
<MaxRange>44.0</MaxRange>
</Ranged>
</Attack>
<Builder>
<Entities datatype="tokens">
-structures/{civ}_wallset_stone
</Entities>
</Builder>
<Cost>
<Resources>
<wood>50</wood>

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_unit_infantry_ranged_slinger">
<Builder>
<Entities datatype="tokens">
-structures/{civ}_wallset_stone
</Entities>
</Builder>
<Cost>
<Resources>
<food>0</food>

View File

@ -11,6 +11,11 @@
<MaxRange>6.0</MaxRange>
</Charge>
</Attack>
<Builder>
<Entities datatype="tokens">
-structures/{civ}_wallset_stone
</Entities>
</Builder>
<Cost>
<Resources>
<wood>60</wood>

View File

@ -48,9 +48,7 @@
</Attack>
<Builder>
<Entities datatype="tokens">
-structures/{civ}_wall
-structures/{civ}_wall_gate
-structures/{civ}_wall_tower
-structures/{civ}_wallset_stone
structures/spart_syssiton
</Entities>
</Builder>

View File

@ -2,9 +2,7 @@
<Entity parent="template_unit_infantry_ranged_javelinist">
<Builder>
<Entities datatype="tokens">
-structures/{civ}_wall
-structures/{civ}_wall_gate
-structures/{civ}_wall_tower
-structures/{civ}_wallset_stone
structures/spart_syssiton
</Entities>
</Builder>

View File

@ -13,9 +13,7 @@
</Attack>
<Builder>
<Entities datatype="tokens">
-structures/{civ}_wall
-structures/{civ}_wall_gate
-structures/{civ}_wall_tower
-structures/{civ}_wallset_stone
structures/spart_syssiton
</Entities>
</Builder>

View File

@ -22,6 +22,7 @@
#include "graphics/Texture.h"
#include "maths/Vector2D.h"
#include "maths/Vector3D.h"
#include "maths/FixedVector3D.h"
#include "ps/Overlay.h" // CColor (TODO: that file has nothing to do with overlays, it should be renamed)
class CTerrain;

View File

@ -87,11 +87,13 @@ void CTooltip::SetupText()
CPos mousepos, offset;
EVAlign anchor;
bool independent;
GUI<bool>::GetSetting(this, "independent", independent);
if (independent)
mousepos = GetMousePos();
else
GUI<CPos>::GetSetting(this, "_mousepos", mousepos);
GUI<CPos>::GetSetting(this, "offset", offset);
GUI<EVAlign>::GetSetting(this, "anchor", anchor);

View File

@ -142,12 +142,12 @@ std::vector<entity_id_t> PickFriendlyEntitiesOnScreen(void* cbdata, int player)
return PickFriendlyEntitiesInRect(cbdata, 0, 0, g_xres, g_yres, player);
}
std::vector<entity_id_t> PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank)
std::vector<entity_id_t> PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank, bool allowFoundations)
{
return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false);
return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false, allowFoundations);
}
CFixedVector3D GetTerrainAtPoint(void* UNUSED(cbdata), int x, int y)
CFixedVector3D GetTerrainAtScreenPoint(void* UNUSED(cbdata), int x, int y)
{
CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true);
return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z));
@ -589,8 +589,8 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint");
scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, int, int, int, int, &PickFriendlyEntitiesInRect>("PickFriendlyEntitiesInRect");
scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, &PickFriendlyEntitiesOnScreen>("PickFriendlyEntitiesOnScreen");
scriptInterface.RegisterFunction<std::vector<entity_id_t>, std::string, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
scriptInterface.RegisterFunction<CFixedVector3D, int, int, &GetTerrainAtPoint>("GetTerrainAtPoint");
scriptInterface.RegisterFunction<std::vector<entity_id_t>, std::string, bool, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
scriptInterface.RegisterFunction<CFixedVector3D, int, int, &GetTerrainAtScreenPoint>("GetTerrainAtScreenPoint");
// Network / game setup functions
scriptInterface.RegisterFunction<void, &StartNetworkGame>("StartNetworkGame");

View File

@ -279,6 +279,7 @@ std::vector<T> ts_make_vector(T* start, size_t size_bytes)
return std::vector<T>(start, start+(size_bytes/sizeof(T)));
}
#define TS_ASSERT_VECTOR_EQUALS_ARRAY(vec1, array) TS_ASSERT_EQUALS(vec1, ts_make_vector((array), sizeof(array)))
#define TS_ASSERT_VECTOR_CONTAINS(vec1, element) TS_ASSERT(std::find((vec1).begin(), (vec1).end(), element) != (vec1).end());
class ScriptInterface;
// Script-based testing setup (defined in test_setup.cpp). Defines TS_* functions.

View File

@ -20,11 +20,11 @@
#include "simulation2/system/Component.h"
#include "ICmpObstruction.h"
#include "ps/CLogger.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/MessageTypes.h"
/**
* Obstruction implementation. This keeps the ICmpPathfinder's model of the world updated when the
* entities move and die, with shapes derived from ICmpFootprint.
@ -49,16 +49,31 @@ public:
STATIC,
UNIT
} m_Type;
entity_pos_t m_Size0; // radius or width
entity_pos_t m_Size1; // radius or depth
flags_t m_TemplateFlags;
// Dynamic state:
bool m_Active; // whether the obstruction is obstructing or just an inactive placeholder
/// Whether the obstruction is actively obstructing or just an inactive placeholder
bool m_Active;
bool m_Moving;
/**
* Unique identifier for grouping obstruction shapes, typically to have member shapes ignore
* each other during obstruction tests. Defaults to the entity ID.
*
* TODO: if needed, perhaps add a mask to specify with respect to which flags members of the
* group should ignore each other.
*/
entity_id_t m_ControlGroup;
entity_id_t m_ControlGroup2;
/// Identifier of this entity's obstruction shape. Contains structure, but should be treated
/// as opaque here.
tag_t m_Tag;
/// Set of flags affecting the behaviour of this entity's obstruction shape.
flags_t m_Flags;
static std::string GetSchema()
@ -139,6 +154,7 @@ public:
m_Tag = tag_t();
m_Moving = false;
m_ControlGroup = GetEntityId();
m_ControlGroup2 = INVALID_ENTITY;
}
virtual void Deinit()
@ -151,6 +167,7 @@ public:
serialize.Bool("active", m_Active);
serialize.Bool("moving", m_Moving);
serialize.NumberU32_Unbounded("control group", m_ControlGroup);
serialize.NumberU32_Unbounded("control group 2", m_ControlGroup2);
serialize.NumberU32_Unbounded("tag", m_Tag.n);
serialize.NumberU8_Unbounded("flags", m_Flags);
}
@ -194,7 +211,7 @@ public:
// Need to create a new pathfinder shape:
if (m_Type == STATIC)
m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(),
data.x, data.z, data.a, m_Size0, m_Size1, m_Flags);
data.x, data.z, data.a, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2);
else
m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(),
data.x, data.z, m_Size0, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
@ -241,10 +258,11 @@ public:
if (!cmpPosition->IsInWorld())
return; // don't need an obstruction
// TODO: code duplication from message handlers
CFixedVector2D pos = cmpPosition->GetPosition2D();
if (m_Type == STATIC)
m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(),
pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags);
pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2);
else
m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(),
pos.X, pos.Y, m_Size0, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
@ -255,6 +273,7 @@ public:
// Delete the obstruction shape
// TODO: code duplication from message handlers
if (m_Tag.valid())
{
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
@ -346,11 +365,21 @@ public:
if (!cmpPathfinder)
return false; // error
// required precondition to use SkipControlGroupsRequireFlagObstructionFilter
if (m_ControlGroup == INVALID_ENTITY)
{
LOGERROR(L"[CmpObstruction] Cannot test for foundation obstructions; primary control group must be valid");
return false;
}
// Get passability class
ICmpPathfinder::pass_class_t passClass = cmpPathfinder->GetPassabilityClass(className);
// Ignore collisions with self, or with non-foundation-blocking obstructions
SkipTagFlagsObstructionFilter filter(m_Tag, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION);
// Ignore collisions within the same control group, or with other non-foundation-blocking shapes.
// Note that, since the control group for each entity defaults to the entity's ID, this is typically
// equivalent to only ignoring the entity's own shape and other non-foundation-blocking shapes.
SkipControlGroupsRequireFlagObstructionFilter filter(m_ControlGroup, m_ControlGroup2,
ICmpObstructionManager::FLAG_BLOCK_FOUNDATION);
if (m_Type == STATIC)
return cmpPathfinder->CheckBuildingPlacement(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, GetEntityId(), passClass);
@ -375,8 +404,18 @@ public:
if (!cmpObstructionManager)
return ret; // error
// Ignore collisions with self, or with non-construction-blocking obstructions
SkipTagFlagsObstructionFilter filter(m_Tag, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
// required precondition to use SkipControlGroupsRequireFlagObstructionFilter
if (m_ControlGroup == INVALID_ENTITY)
{
LOGERROR(L"[CmpObstruction] Cannot test for construction obstructions; primary control group must be valid");
return ret;
}
// Ignore collisions within the same control group, or with other non-construction-blocking shapes.
// Note that, since the control group for each entity defaults to the entity's ID, this is typically
// equivalent to only ignoring the entity's own shape and other non-construction-blocking shapes.
SkipControlGroupsRequireFlagObstructionFilter filter(m_ControlGroup, m_ControlGroup2,
ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
if (m_Type == STATIC)
cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, &ret);
@ -401,12 +440,41 @@ public:
virtual void SetControlGroup(entity_id_t group)
{
m_ControlGroup = group;
UpdateControlGroups();
}
if (m_Tag.valid() && m_Type == UNIT)
virtual void SetControlGroup2(entity_id_t group2)
{
m_ControlGroup2 = group2;
UpdateControlGroups();
}
virtual entity_id_t GetControlGroup()
{
return m_ControlGroup;
}
virtual entity_id_t GetControlGroup2()
{
return m_ControlGroup2;
}
void UpdateControlGroups()
{
if (m_Tag.valid())
{
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager)
cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup);
{
if (m_Type == UNIT)
{
cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup);
}
else if (m_Type == STATIC)
{
cmpObstructionManager->SetStaticControlGroup(m_Tag, m_ControlGroup, m_ControlGroup2);
}
}
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2011 Wildfire Games.
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -32,6 +32,7 @@
#include "ps/Overlay.h"
#include "ps/Profile.h"
#include "renderer/Scene.h"
#include "ps/CLogger.h"
// Externally, tags are opaque non-zero positive integers.
// Internally, they are tagged (by shape) indexes into shape lists.
@ -65,6 +66,8 @@ struct StaticShape
CFixedVector2D u, v; // orthogonal unit vectors - axes of local coordinate space
entity_pos_t hw, hh; // half width/height in local coordinate space
ICmpObstructionManager::flags_t flags;
entity_id_t group;
entity_id_t group2;
};
/**
@ -102,6 +105,8 @@ struct SerializeStaticShape
serialize.NumberFixed_Unbounded("hw", value.hw);
serialize.NumberFixed_Unbounded("hh", value.hh);
serialize.NumberU8_Unbounded("flags", value.flags);
serialize.NumberU32_Unbounded("group", value.group);
serialize.NumberU32_Unbounded("group2", value.group2);
}
};
@ -257,14 +262,14 @@ public:
return UNIT_INDEX_TO_TAG(id);
}
virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h, flags_t flags)
virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h, flags_t flags, entity_id_t group, entity_id_t group2 /* = INVALID_ENTITY */)
{
fixed s, c;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s);
CFixedVector2D v(s, c);
StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags };
StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags, group, group2 };
u32 id = m_StaticShapeNext++;
m_StaticShapes[id] = shape;
MakeDirtyStatic(flags);
@ -367,6 +372,18 @@ public:
}
}
virtual void SetStaticControlGroup(tag_t tag, entity_id_t group, entity_id_t group2)
{
ENSURE(TAG_IS_VALID(tag) && TAG_IS_STATIC(tag));
if (TAG_IS_STATIC(tag))
{
StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
shape.group = group;
shape.group2 = group2;
}
}
virtual void RemoveShape(tag_t tag)
{
ENSURE(TAG_IS_VALID(tag));
@ -533,7 +550,7 @@ bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, enti
std::map<u32, UnitShape>::iterator it = m_UnitShapes.find(unitShapes[i]);
ENSURE(it != m_UnitShapes.end());
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
continue;
CFixedVector2D center(it->second.x, it->second.z);
@ -548,7 +565,7 @@ bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, enti
std::map<u32, StaticShape>::iterator it = m_StaticShapes.find(staticShapes[i]);
ENSURE(it != m_StaticShapes.end());
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
continue;
CFixedVector2D center(it->second.x, it->second.z);
@ -592,7 +609,7 @@ bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filte
for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
@ -608,7 +625,7 @@ bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filte
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
@ -649,7 +666,7 @@ bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,
for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
continue;
entity_pos_t r1 = it->second.r;
@ -665,7 +682,7 @@ bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
@ -869,7 +886,7 @@ void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter
std::map<u32, UnitShape>::iterator it = m_UnitShapes.find(unitShapes[i]);
ENSURE(it != m_UnitShapes.end());
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
continue;
entity_pos_t r = it->second.r;
@ -890,7 +907,7 @@ void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter
std::map<u32, StaticShape>::iterator it = m_StaticShapes.find(staticShapes[i]);
ENSURE(it != m_StaticShapes.end());
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
continue;
entity_pos_t r = it->second.hw + it->second.hh; // overestimate the max dist of an edge from the center

View File

@ -869,7 +869,7 @@ void CCmpRallyPointRenderer::ReduceSegmentsByVisibility(std::vector<CVector2D>&
// process from there on until the entire line is checked. The output is the array of base nodes.
std::vector<CVector2D> newCoords;
StationaryObstructionFilter obstructionFilter;
StationaryOnlyObstructionFilter obstructionFilter;
entity_pos_t lineRadius = fixed::FromFloat(m_LineThickness);
ICmpPathfinder::pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass);

View File

@ -357,7 +357,7 @@ bool CCmpTemplateManager::LoadTemplateFile(const std::string& templateName, int
}
// Load the new file into the template data (overriding parent values)
CParamNode::LoadXML(m_TemplateFileData[templateName], xero);
CParamNode::LoadXML(m_TemplateFileData[templateName], xero, wstring_from_utf8(templateName).c_str());
return true;
}
@ -376,7 +376,8 @@ void CCmpTemplateManager::ConstructTemplateActor(const std::string& actorName, C
out = m_TemplateFileData[templateName];
// Initialise the actor's name and make it an Atlas selectable entity.
std::string name = utf8_from_wstring(CParamNode::EscapeXMLString(wstring_from_utf8(actorName)));
std::wstring actorNameW = wstring_from_utf8(actorName);
std::string name = utf8_from_wstring(CParamNode::EscapeXMLString(actorNameW));
std::string xml = "<Entity>"
"<VisualActor><Actor>" + name + "</Actor></VisualActor>"
"<Selectable>"
@ -385,7 +386,7 @@ void CCmpTemplateManager::ConstructTemplateActor(const std::string& actorName, C
"</Selectable>"
"</Entity>";
CParamNode::LoadXMLString(out, xml.c_str());
CParamNode::LoadXMLString(out, xml.c_str(), actorNameW.c_str());
}
static Status AddToTemplates(const VfsPath& pathname, const FileInfo& UNUSED(fileInfo), const uintptr_t cbData)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2011 Wildfire Games.
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -29,4 +29,7 @@ DEFINE_INTERFACE_METHOD_1("SetActive", void, ICmpObstruction, SetActive, bool)
DEFINE_INTERFACE_METHOD_1("SetDisableBlockMovementPathfinding", void, ICmpObstruction, SetDisableBlockMovementPathfinding, bool)
DEFINE_INTERFACE_METHOD_0("GetBlockMovementFlag", bool, ICmpObstruction, GetBlockMovementFlag)
DEFINE_INTERFACE_METHOD_1("SetControlGroup", void, ICmpObstruction, SetControlGroup, entity_id_t)
DEFINE_INTERFACE_METHOD_0("GetControlGroup", entity_id_t, ICmpObstruction, GetControlGroup)
DEFINE_INTERFACE_METHOD_1("SetControlGroup2", void, ICmpObstruction, SetControlGroup2, entity_id_t)
DEFINE_INTERFACE_METHOD_0("GetControlGroup2", entity_id_t, ICmpObstruction, GetControlGroup2)
END_INTERFACE_WRAPPER(Obstruction)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2011 Wildfire Games.
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -44,6 +44,7 @@ public:
/**
* Test whether this entity is colliding with any obstruction that are set to
* block the creation of foundations.
* @param ignoredEntities List of entities to ignore during the test.
* @return true if foundation is valid (not obstructed)
*/
virtual bool CheckFoundation(std::string className) = 0;
@ -70,6 +71,12 @@ public:
*/
virtual void SetControlGroup(entity_id_t group) = 0;
/// See SetControlGroup.
virtual entity_id_t GetControlGroup() = 0;
virtual void SetControlGroup2(entity_id_t group2) = 0;
virtual entity_id_t GetControlGroup2() = 0;
DECLARE_INTERFACE_TYPE(Obstruction)
};

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2011 Wildfire Games.
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -93,19 +93,24 @@ public:
/**
* Register a static shape.
*
* @param ent entity ID associated with this shape (or INVALID_ENTITY if none)
* @param x,z coordinates of center, in world space
* @param a angle of rotation (clockwise from +Z direction)
* @param w width (size along X axis)
* @param h height (size along Z axis)
* @param flags a set of EFlags values
* @param group primary control group of the shape. Must be a valid control group ID.
* @param group2 Optional; secondary control group of the shape. Defaults to INVALID_ENTITY.
* @return a valid tag for manipulating the shape
* @see StaticShape
*/
virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h, flags_t flags) = 0;
virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a,
entity_pos_t w, entity_pos_t h, flags_t flags, entity_id_t group, entity_id_t group2 = INVALID_ENTITY) = 0;
/**
* Register a unit shape.
*
* @param ent entity ID associated with this shape (or INVALID_ENTITY if none)
* @param x,z coordinates of center, in world space
* @param r radius of circle or half the unit's width/height
@ -115,7 +120,8 @@ public:
* @return a valid tag for manipulating the shape
* @see UnitShape
*/
virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t r, flags_t flags, entity_id_t group) = 0;
virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t r, flags_t flags,
entity_id_t group) = 0;
/**
* Adjust the position and angle of an existing shape.
@ -140,6 +146,13 @@ public:
*/
virtual void SetUnitControlGroup(tag_t tag, entity_id_t group) = 0;
/**
* Sets the control group of a static shape.
* @param tag Tag of the shape to set the control group for. Must be a valid and static shape tag.
* @param group Control group entity ID.
*/
virtual void SetStaticControlGroup(tag_t tag, entity_id_t group, entity_id_t group2) = 0;
/**
* Remove an existing shape. The tag will be made invalid and must not be used after this.
* @param tag tag of shape (must be valid)
@ -162,7 +175,7 @@ public:
/**
* Collision test a static square shape against the current set of shapes.
* @param filter filter to restrict the shapes that are counted
* @param filter filter to restrict the shapes that are being tested against
* @param x X coordinate of center
* @param z Z coordinate of center
* @param a angle of rotation (clockwise from +Z direction)
@ -176,12 +189,15 @@ public:
std::vector<entity_id_t>* out) = 0;
/**
* Collision test a unit shape against the current set of shapes.
* @param filter filter to restrict the shapes that are counted
* @param x X coordinate of center
* @param z Z coordinate of center
* @param r radius (half the unit's width/height)
* Collision test a unit shape against the current set of registered shapes, and optionally writes a list of the colliding
* shapes' entities to an output list.
*
* @param filter filter to restrict the shapes that are being tested against
* @param x X coordinate of shape's center
* @param z Z coordinate of shape's center
* @param r radius of the shape (half the unit's width/height)
* @param out if non-NULL, all colliding shapes' entities will be added to this list
*
* @return true if there is a collision
*/
virtual bool TestUnitShape(const IObstructionTestFilter& filter,
@ -274,34 +290,37 @@ public:
virtual ~IObstructionTestFilter() {}
/**
* Return true if the shape should be counted for collisions.
* Return true if the shape with the specified parameters should be tested for collisions.
* This is called for all shapes that would collide, and also for some that wouldn't.
*
* @param tag tag of shape being tested
* @param flags set of EFlags for the shape
* @param group the control group (typically the shape's unit, or the unit's formation controller, or 0)
* @param group the control group of the shape (typically the shape's unit, or the unit's formation controller, or 0)
* @param group2 an optional secondary control group of the shape, or INVALID_ENTITY if none specified. Currently
* exists only for static shapes.
*/
virtual bool Allowed(tag_t tag, flags_t flags, entity_id_t group) const = 0;
virtual bool TestShape(tag_t tag, flags_t flags, entity_id_t group, entity_id_t group2) const = 0;
};
/**
* Obstruction test filter that accepts all shapes.
* Obstruction test filter that will test against all shapes.
*/
class NullObstructionFilter : public IObstructionTestFilter
{
public:
virtual bool Allowed(tag_t UNUSED(tag), flags_t UNUSED(flags), entity_id_t UNUSED(group)) const
virtual bool TestShape(tag_t UNUSED(tag), flags_t UNUSED(flags), entity_id_t UNUSED(group), entity_id_t UNUSED(group2)) const
{
return true;
}
};
/**
* Obstruction test filter that accepts all non-moving shapes.
* Obstruction test filter that will test only against stationary (i.e. non-moving) shapes.
*/
class StationaryObstructionFilter : public IObstructionTestFilter
class StationaryOnlyObstructionFilter : public IObstructionTestFilter
{
public:
virtual bool Allowed(tag_t UNUSED(tag), flags_t flags, entity_id_t UNUSED(group)) const
virtual bool TestShape(tag_t UNUSED(tag), flags_t flags, entity_id_t UNUSED(group), entity_id_t UNUSED(group2)) const
{
return !(flags & ICmpObstructionManager::FLAG_MOVING);
}
@ -309,22 +328,21 @@ public:
/**
* Obstruction test filter that reject shapes in a given control group,
* and optionally rejects moving shapes,
* and rejects shapes that don't block unit movement.
* and rejects shapes that don't block unit movement, and optionally rejects moving shapes.
*/
class ControlGroupMovementObstructionFilter : public IObstructionTestFilter
{
bool m_AvoidMoving;
entity_id_t m_Group;
public:
ControlGroupMovementObstructionFilter(bool avoidMoving, entity_id_t group) :
m_AvoidMoving(avoidMoving), m_Group(group)
{
}
{}
virtual bool Allowed(tag_t UNUSED(tag), flags_t flags, entity_id_t group) const
virtual bool TestShape(tag_t UNUSED(tag), flags_t flags, entity_id_t group, entity_id_t group2) const
{
if (group == m_Group)
if (group == m_Group || (group2 != INVALID_ENTITY && group2 == m_Group))
return false;
if (!(flags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT))
return false;
@ -335,7 +353,52 @@ public:
};
/**
* Obstruction test filter that rejects a specific shape.
* Obstruction test filter that will test only against shapes that:
* - are part of neither one of the specified control groups
* - AND have at least one of the specified flags set.
*
* The first (primary) control group to reject shapes from must be specified and valid. The secondary
* control group to reject entities from may be set to INVALID_ENTITY to not use it.
*
* This filter is useful to e.g. allow foundations within the same control group to be placed and
* constructed arbitrarily close together (e.g. for wall pieces that need to link up tightly).
*/
class SkipControlGroupsRequireFlagObstructionFilter : public IObstructionTestFilter
{
entity_id_t m_Group;
entity_id_t m_Group2;
flags_t m_Mask;
public:
SkipControlGroupsRequireFlagObstructionFilter(entity_id_t group1, entity_id_t group2, flags_t mask) :
m_Group(group1), m_Group2(group2), m_Mask(mask)
{
// the primary control group to filter out must be valid
ENSURE(m_Group != INVALID_ENTITY);
// for simplicity, if m_Group2 is INVALID_ENTITY (i.e. not used), then set it equal to m_Group
// so that we have fewer special cases to consider in TestShape().
if (m_Group2 == INVALID_ENTITY)
m_Group2 = m_Group;
}
virtual bool TestShape(tag_t UNUSED(tag), flags_t flags, entity_id_t group, entity_id_t group2) const
{
// To be included in the testing, a shape must have at least one of the flags in m_Mask set, and its
// primary control group must be valid and must equal neither our primary nor secondary control group.
bool includeInTesting = ((flags & m_Mask) != 0 && group != m_Group && group != m_Group2);
// If the shape being tested has a valid secondary control group, exclude it from testing if it
// matches either our primary or secondary control group.
if (group2 != INVALID_ENTITY)
includeInTesting = (includeInTesting && group2 != m_Group && group2 != m_Group2);
return includeInTesting;
}
};
/**
* Obstruction test filter that will test only against shapes that do not have the specified tag set.
*/
class SkipTagObstructionFilter : public IObstructionTestFilter
{
@ -345,25 +408,27 @@ public:
{
}
virtual bool Allowed(tag_t tag, flags_t UNUSED(flags), entity_id_t UNUSED(group)) const
virtual bool TestShape(tag_t tag, flags_t UNUSED(flags), entity_id_t UNUSED(group), entity_id_t UNUSED(group2)) const
{
return tag.n != m_Tag.n;
}
};
/**
* Obstruction test filter that rejects a specific shape, and requires the given flags.
* Obstruction test filter that will test only against shapes that:
* - do not have the specified tag
* - AND have at least one of the specified flags set.
*/
class SkipTagFlagsObstructionFilter : public IObstructionTestFilter
class SkipTagRequireFlagsObstructionFilter : public IObstructionTestFilter
{
tag_t m_Tag;
flags_t m_Mask;
public:
SkipTagFlagsObstructionFilter(tag_t tag, flags_t mask) : m_Tag(tag), m_Mask(mask)
SkipTagRequireFlagsObstructionFilter(tag_t tag, flags_t mask) : m_Tag(tag), m_Mask(mask)
{
}
virtual bool Allowed(tag_t tag, flags_t flags, entity_id_t UNUSED(group)) const
virtual bool TestShape(tag_t tag, flags_t flags, entity_id_t UNUSED(group), entity_id_t UNUSED(group2)) const
{
return (tag.n != m_Tag.n && (flags & m_Mask) != 0);
}

View File

@ -0,0 +1,477 @@
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "simulation2/system/ComponentTest.h"
#include "simulation2/components/ICmpObstructionManager.h"
class TestCmpObstructionManager : public CxxTest::TestSuite
{
typedef ICmpObstructionManager::tag_t tag_t;
typedef ICmpObstructionManager::ObstructionSquare ObstructionSquare;
// some variables for setting up a scene with 3 shapes
entity_id_t ent1, ent2, ent3; // entity IDs
entity_angle_t ent1a, ent2r, ent3r; // angles/radiuses
entity_pos_t ent1x, ent1z, ent1w, ent1h, // positions/dimensions
ent2x, ent2z,
ent3x, ent3z;
entity_id_t ent1g1, ent1g2, ent2g, ent3g; // control groups
tag_t shape1, shape2, shape3;
ICmpObstructionManager* cmp;
ComponentTestHelper* testHelper;
public:
void setUp()
{
CXeromyces::Startup();
CxxTest::setAbortTestOnFail(true);
// set up a simple scene with some predefined obstruction shapes
// (we can't position shapes on the origin because the world bounds must range
// from 0 to X, so instead we'll offset things by, say, 10).
ent1 = 1;
ent1a = fixed::Zero();
ent1w = fixed::FromFloat(4);
ent1h = fixed::FromFloat(2);
ent1x = fixed::FromInt(10);
ent1z = fixed::FromInt(10);
ent1g1 = ent1;
ent1g2 = INVALID_ENTITY;
ent2 = 2;
ent2r = fixed::FromFloat(1);
ent2x = ent1x;
ent2z = ent1z;
ent2g = ent1g1;
ent3 = 3;
ent3r = fixed::FromFloat(3);
ent3x = ent2x;
ent3z = ent2z + ent2r + ent3r; // ensure it just touches the border of ent2
ent3g = ent3;
testHelper = new ComponentTestHelper;
cmp = testHelper->Add<ICmpObstructionManager>(CID_ObstructionManager, "");
cmp->SetBounds(fixed::FromInt(0), fixed::FromInt(0), fixed::FromInt(1000), fixed::FromInt(1000));
shape1 = cmp->AddStaticShape(ent1, ent1x, ent1z, ent1a, ent1w, ent1h,
ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION |
ICmpObstructionManager::FLAG_BLOCK_MOVEMENT |
ICmpObstructionManager::FLAG_MOVING, ent1g1, ent1g2);
shape2 = cmp->AddUnitShape(ent2, ent2x, ent2z, ent2r,
ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION |
ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent2g);
shape3 = cmp->AddUnitShape(ent3, ent3x, ent3z, ent3r,
ICmpObstructionManager::FLAG_BLOCK_MOVEMENT |
ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent3g);
}
void tearDown()
{
delete testHelper;
cmp = NULL; // not our responsibility to deallocate
CXeromyces::Terminate();
}
/**
* Verifies the collision testing procedure. Collision-tests some simple shapes against the shapes registered in
* the scene, and verifies the result of the test against the expected value.
*/
void test_simple_collisions()
{
std::vector<entity_id_t> out;
NullObstructionFilter nullFilter;
// Collision-test a simple shape nested inside shape3 against all shapes in the scene. Since the tested shape
// overlaps only with shape 3, we should find only shape 3 in the result.
cmp->TestUnitShape(nullFilter, ent3x, ent3z, fixed::FromInt(1), &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent3, out[0]);
out.clear();
cmp->TestStaticShape(nullFilter, ent3x, ent3z, fixed::Zero(), fixed::FromInt(1), fixed::FromInt(1), &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent3, out[0]);
out.clear();
// Similarly, collision-test a simple shape nested inside both shape1 and shape2. Since the tested shape overlaps
// only with shapes 1 and 2, those are the only ones we should find in the result.
cmp->TestUnitShape(nullFilter, ent2x, ent2z, ent2r/2, &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent1);
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
out.clear();
cmp->TestStaticShape(nullFilter, ent2x, ent2z, fixed::Zero(), ent2r, ent2r, &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent1);
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
out.clear();
}
/**
* Verifies the behaviour of the null obstruction filter. Tests with this filter will be performed against all
* registered shapes.
*/
void test_filter_null()
{
std::vector<entity_id_t> out;
// Collision test a scene-covering shape against all shapes in the scene. We should find all registered shapes
// in the result.
NullObstructionFilter nullFilter;
cmp->TestUnitShape(nullFilter, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(3U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent1);
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
cmp->TestStaticShape(nullFilter, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(3U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent1);
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
}
/**
* Verifies the behaviour of the StationaryOnlyObstructionFilter. Tests with this filter will be performed only
* against non-moving (stationary) shapes.
*/
void test_filter_stationary_only()
{
std::vector<entity_id_t> out;
// Collision test a scene-covering shape against all shapes in the scene, but skipping shapes that are moving,
// i.e. shapes that have the MOVING flag. Since only shape 1 is flagged as moving, we should find
// shapes 2 and 3 in each case.
StationaryOnlyObstructionFilter ignoreMoving;
cmp->TestUnitShape(ignoreMoving, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
cmp->TestStaticShape(ignoreMoving, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
}
/**
* Verifies the behaviour of the SkipTagObstructionFilter. Tests with this filter will be performed against
* all registered shapes that do not have the specified tag set.
*/
void test_filter_skip_tag()
{
std::vector<entity_id_t> out;
// Collision-test shape 2's obstruction shape against all shapes in the scene, but skipping tests against
// shape 2. Since shape 2 overlaps only with shape 1, we should find only shape 1's entity ID in the result.
SkipTagObstructionFilter ignoreShape2(shape2);
cmp->TestUnitShape(ignoreShape2, ent2x, ent2z, ent2r/2, &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent1, out[0]);
out.clear();
cmp->TestStaticShape(ignoreShape2, ent2x, ent2z, fixed::Zero(), ent2r, ent2r, &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent1, out[0]);
out.clear();
}
/**
* Verifies the behaviour of the SkipTagFlagsObstructionFilter. Tests with this filter will be performed against
* all registered shapes that do not have the specified tag set, and that have at least one of required flags set.
*/
void test_filter_skip_tag_require_flag()
{
std::vector<entity_id_t> out;
// Collision-test a scene-covering shape against all shapes in the scene, but skipping tests against shape 1
// and requiring the BLOCK_MOVEMENT flag. Since shape 1 is being ignored and shape 2 does not have the required
// flag, we should find only shape 3 in the results.
SkipTagRequireFlagsObstructionFilter skipShape1RequireBlockMovement(shape1, ICmpObstructionManager::FLAG_BLOCK_MOVEMENT);
cmp->TestUnitShape(skipShape1RequireBlockMovement, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent3, out[0]);
out.clear();
cmp->TestStaticShape(skipShape1RequireBlockMovement, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent3, out[0]);
out.clear();
// If we now do the same test, but require at least one of the entire set of available filters, we should find
// all shapes that are not shape 1 and that have at least one flag set. Since all shapes in our testing scene
// have at least one flag set, we should find shape 2 and shape 3 in the results.
SkipTagRequireFlagsObstructionFilter skipShape1RequireAnyFlag(shape1, (ICmpObstructionManager::flags_t) -1);
cmp->TestUnitShape(skipShape1RequireAnyFlag, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
cmp->TestStaticShape(skipShape1RequireAnyFlag, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
// And if we now do the same test yet again, but specify an empty set of flags, then it becomes impossible for
// any shape to have at least one of the required flags, and we should hence find no shapes in the result.
SkipTagRequireFlagsObstructionFilter skipShape1RejectAll(shape1, 0U);
cmp->TestUnitShape(skipShape1RejectAll, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(0U, out.size());
out.clear();
cmp->TestStaticShape(skipShape1RejectAll, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(0U, out.size());
out.clear();
}
/**
* Verifies the behaviour of SkipControlGroupsRequireFlagObstructionFilter. Tests with this filter will be performed
* against all registered shapes that are members of neither specified control groups, and that have at least one of
* the specified flags set.
*/
void test_filter_skip_controlgroups_require_flag()
{
std::vector<entity_id_t> out;
// Collision-test a shape that overlaps the entire scene, but ignoring shapes from shape1's control group
// (which also includes shape 2), and requiring that either the BLOCK_FOUNDATION or the
// BLOCK_CONSTRUCTION flag is set, or both. Since shape 1 and shape 2 both belong to shape 1's control
// group, and shape 3 has the BLOCK_FOUNDATION flag (but not BLOCK_CONSTRUCTION), we should find only
// shape 3 in the result.
SkipControlGroupsRequireFlagObstructionFilter skipGroup1ReqFoundConstr(ent1g1, INVALID_ENTITY,
ICmpObstructionManager::FLAG_BLOCK_FOUNDATION | ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
cmp->TestUnitShape(skipGroup1ReqFoundConstr, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent3, out[0]);
out.clear();
cmp->TestStaticShape(skipGroup1ReqFoundConstr, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent3, out[0]);
out.clear();
// Perform the same test, but now also exclude shape 3's control group (in addition to shape 1's control
// group). Despite shape 3 having at least one of the required flags set, it should now also be ignored,
// yielding an empty result set.
SkipControlGroupsRequireFlagObstructionFilter skipGroup1And3ReqFoundConstr(ent1g1, ent3g,
ICmpObstructionManager::FLAG_BLOCK_FOUNDATION | ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
cmp->TestUnitShape(skipGroup1And3ReqFoundConstr, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(0U, out.size());
out.clear();
cmp->TestStaticShape(skipGroup1And3ReqFoundConstr, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(0U, out.size());
out.clear();
// Same test, but this time excluding only shape 3's control group, and requiring any of the available flags
// to be set. Since both shape 1 and shape 2 have at least one flag set and are both in a different control
// group, we should find them in the result.
SkipControlGroupsRequireFlagObstructionFilter skipGroup3RequireAnyFlag(ent3g, INVALID_ENTITY,
(ICmpObstructionManager::flags_t) -1);
cmp->TestUnitShape(skipGroup3RequireAnyFlag, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent1);
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
out.clear();
cmp->TestStaticShape(skipGroup3RequireAnyFlag, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent1);
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
out.clear();
// Finally, the same test as the one directly above, now with an empty set of required flags. Since it now becomes
// impossible for shape 1 and shape 2 to have at least one of the required flags set, and shape 3 is excluded by
// virtue of the control group filtering, we should find an empty result.
SkipControlGroupsRequireFlagObstructionFilter skipGroup3RequireNoFlags(ent3g, INVALID_ENTITY, 0U);
cmp->TestUnitShape(skipGroup3RequireNoFlags, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(0U, out.size());
out.clear();
cmp->TestStaticShape(skipGroup3RequireNoFlags, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(0U, out.size());
out.clear();
// ------------------------------------------------------------------------------------
// In the tests up until this point, the shapes have all been filtered out based on their primary control group.
// Now, to verify that shapes are also filtered out based on their secondary control groups, add a fourth shape
// with arbitrarily-chosen dual control groups, and also change shape 1's secondary control group to another
// arbitrarily-chosen control group. Then, do a scene-covering collision test while filtering out a combination
// of shape 1's secondary control group, and one of shape 4's control groups. We should find neither ent1 nor ent4
// in the result.
entity_id_t ent4 = 4,
ent4g1 = 17,
ent4g2 = 19,
ent1g2_new = 18; // new secondary control group for entity 1
entity_pos_t ent4x = fixed::FromInt(4),
ent4z = fixed::Zero(),
ent4w = fixed::FromInt(1),
ent4h = fixed::FromInt(1);
entity_angle_t ent4a = fixed::FromDouble(M_PI/3);
cmp->AddStaticShape(ent4, ent4x, ent4z, ent4a, ent4w, ent4h, ICmpObstructionManager::FLAG_BLOCK_PATHFINDING, ent4g1, ent4g2);
cmp->SetStaticControlGroup(shape1, ent1g1, ent1g2_new);
// Exclude shape 1's and shape 4's secondary control groups from testing, and require any available flag to be set.
// Since neither shape 2 nor shape 3 are part of those control groups and both have at least one available flag set,
// the results should only those two shapes' entities.
SkipControlGroupsRequireFlagObstructionFilter skipGroup1SecAnd4SecRequireAny(ent1g2_new, ent4g2,
(ICmpObstructionManager::flags_t) -1);
cmp->TestUnitShape(skipGroup1SecAnd4SecRequireAny, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
cmp->TestStaticShape(skipGroup1SecAnd4SecRequireAny, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
// Same as the above, but now exclude shape 1's secondary and shape 4's primary control group, while still requiring
// any available flag to be set. (Note that the test above used shape 4's secondary control group). Results should
// remain the same.
SkipControlGroupsRequireFlagObstructionFilter skipGroup1SecAnd4PrimRequireAny(ent1g2_new, ent4g1,
(ICmpObstructionManager::flags_t) -1);
cmp->TestUnitShape(skipGroup1SecAnd4PrimRequireAny, ent1x, ent1z, fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
cmp->TestStaticShape(skipGroup1SecAnd4PrimRequireAny, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
TS_ASSERT_EQUALS(2U, out.size());
TS_ASSERT_VECTOR_CONTAINS(out, ent2);
TS_ASSERT_VECTOR_CONTAINS(out, ent3);
out.clear();
cmp->SetStaticControlGroup(shape1, ent1g1, ent1g2); // restore shape 1's original secondary control group
}
void test_adjacent_shapes()
{
std::vector<entity_id_t> out;
NullObstructionFilter nullFilter;
SkipTagObstructionFilter ignoreShape1(shape1);
SkipTagObstructionFilter ignoreShape2(shape2);
SkipTagObstructionFilter ignoreShape3(shape3);
// Collision-test a shape that is perfectly adjacent to shape3. This should be counted as a hit according to
// the code at the time of writing.
entity_angle_t ent4a = fixed::FromDouble(M_PI); // rotated 180 degrees, should not affect collision test
entity_pos_t ent4w = fixed::FromInt(2),
ent4h = fixed::FromInt(1),
ent4x = ent3x + ent3r + ent4w/2, // make ent4 adjacent to ent3
ent4z = ent3z;
cmp->TestStaticShape(nullFilter, ent4x, ent4z, ent4a, ent4w, ent4h, &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent3, out[0]);
out.clear();
cmp->TestUnitShape(nullFilter, ent4x, ent4z, ent4w/2, &out);
TS_ASSERT_EQUALS(1U, out.size());
TS_ASSERT_EQUALS(ent3, out[0]);
out.clear();
// now do the same tests, but move the shape a little bit to the right so that it doesn't touch anymore
cmp->TestStaticShape(nullFilter, ent4x + fixed::FromFloat(1e-5f), ent4z, ent4a, ent4w, ent4h, &out);
TS_ASSERT_EQUALS(0U, out.size());
out.clear();
cmp->TestUnitShape(nullFilter, ent4x + fixed::FromFloat(1e-5f), ent4z, ent4w/2, &out);
TS_ASSERT_EQUALS(0U, out.size());
out.clear();
}
/**
* Verifies that fetching the registered shapes from the obstruction manager yields the correct results.
*/
void test_get_obstruction()
{
ObstructionSquare obSquare1 = cmp->GetObstruction(shape1);
ObstructionSquare obSquare2 = cmp->GetObstruction(shape2);
ObstructionSquare obSquare3 = cmp->GetObstruction(shape3);
TS_ASSERT_EQUALS(obSquare1.hh, ent1h/2);
TS_ASSERT_EQUALS(obSquare1.hw, ent1w/2);
TS_ASSERT_EQUALS(obSquare1.x, ent1x);
TS_ASSERT_EQUALS(obSquare1.z, ent1z);
TS_ASSERT_EQUALS(obSquare1.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0)));
TS_ASSERT_EQUALS(obSquare1.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1)));
TS_ASSERT_EQUALS(obSquare2.hh, ent2r);
TS_ASSERT_EQUALS(obSquare2.hw, ent2r);
TS_ASSERT_EQUALS(obSquare2.x, ent2x);
TS_ASSERT_EQUALS(obSquare2.z, ent2z);
TS_ASSERT_EQUALS(obSquare2.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0)));
TS_ASSERT_EQUALS(obSquare2.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1)));
TS_ASSERT_EQUALS(obSquare3.hh, ent3r);
TS_ASSERT_EQUALS(obSquare3.hw, ent3r);
TS_ASSERT_EQUALS(obSquare3.x, ent3x);
TS_ASSERT_EQUALS(obSquare3.z, ent3z);
TS_ASSERT_EQUALS(obSquare3.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0)));
TS_ASSERT_EQUALS(obSquare3.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1)));
}
};

View File

@ -27,6 +27,7 @@
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpSelectable.h"
#include "simulation2/components/ICmpVisual.h"
#include "ps/CLogger.h"
std::vector<entity_id_t> EntitySelection::PickEntitiesAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables)
{
@ -161,7 +162,9 @@ std::vector<entity_id_t> EntitySelection::PickEntitiesInRect(CSimulation2& simul
return hitEnts;
}
std::vector<entity_id_t> EntitySelection::PickSimilarEntities(CSimulation2& simulation, const CCamera& camera, const std::string& templateName, player_id_t owner, bool includeOffScreen, bool matchRank, bool allowEditorSelectables)
std::vector<entity_id_t> EntitySelection::PickSimilarEntities(CSimulation2& simulation, const CCamera& camera,
const std::string& templateName, player_id_t owner, bool includeOffScreen, bool matchRank,
bool allowEditorSelectables, bool allowFoundations)
{
CmpPtr<ICmpTemplateManager> cmpTemplateManager(simulation, SYSTEM_ENTITY);
CmpPtr<ICmpRangeManager> cmpRangeManager(simulation, SYSTEM_ENTITY);
@ -179,8 +182,11 @@ std::vector<entity_id_t> EntitySelection::PickSimilarEntities(CSimulation2& simu
if (matchRank)
{
// Exact template name matching
if (cmpTemplateManager->GetCurrentTemplateName(ent) != templateName)
// Exact template name matching, optionally also allowing foundations
std::string curTemplateName = cmpTemplateManager->GetCurrentTemplateName(ent);
bool matches = (curTemplateName == templateName ||
(allowFoundations && curTemplateName.substr(0, 11) == "foundation|" && curTemplateName.substr(11) == templateName));
if (!matches)
continue;
}

View File

@ -76,12 +76,15 @@ std::vector<entity_id_t> PickEntitiesInRect(CSimulation2& simulation, const CCam
* @param matchRank if true, only entities that exactly match templateName will be selected,
* else entities with matching SelectionGroupName will be selected.
* @param allowEditorSelectables if true, all entities with the ICmpSelectable interface
* will be selected (including decorative actors), else only those selectable ingame.
* will be selected (including decorative actors), else only those selectable in-game.
* @param allowFoundations if true, foundations are also included in the results. Only takes
* effect when matchRank = true.
*
* @return unordered list of selected entities.
* @see ICmpIdentity
*/
std::vector<entity_id_t> PickSimilarEntities(CSimulation2& simulation, const CCamera& camera, const std::string& templateName, player_id_t owner, bool includeOffScreen, bool matchRank, bool allowEditorSelectables);
std::vector<entity_id_t> PickSimilarEntities(CSimulation2& simulation, const CCamera& camera, const std::string& templateName,
player_id_t owner, bool includeOffScreen, bool matchRank, bool allowEditorSelectables, bool allowFoundations);
} // namespace

View File

@ -44,9 +44,9 @@ CParamNode::CParamNode(bool isOk) :
{
}
void CParamNode::LoadXML(CParamNode& ret, const XMBFile& xmb)
void CParamNode::LoadXML(CParamNode& ret, const XMBFile& xmb, const wchar_t* sourceIdentifier /*= NULL*/)
{
ret.ApplyLayer(xmb, xmb.GetRoot());
ret.ApplyLayer(xmb, xmb.GetRoot(), sourceIdentifier);
}
void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path)
@ -56,22 +56,22 @@ void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path)
if (ok != PSRETURN_OK)
return; // (Xeromyces already logged an error)
LoadXML(ret, xero);
LoadXML(ret, xero, path.string().c_str());
}
PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml)
PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier /*=NULL*/)
{
CXeromyces xero;
PSRETURN ok = xero.LoadString(xml);
if (ok != PSRETURN_OK)
return ok;
ret.ApplyLayer(xero, xero.GetRoot());
ret.ApplyLayer(xero, xero.GetRoot(), sourceIdentifier);
return PSRETURN_OK;
}
void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element)
void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const wchar_t* sourceIdentifier /*= NULL*/)
{
ResetScriptVal();
@ -128,8 +128,8 @@ void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element)
if (tokenIt != tokens.end())
tokens.erase(tokenIt);
else
LOGWARNING(L"[ParamNode] Could not remove token '%ls' from node '%hs'; not present in list nor inherited (possible typo?)",
newTokens[i].substr(1).c_str(), name.c_str());
LOGWARNING(L"[ParamNode] Could not remove token '%ls' from node '%hs'%ls; not present in list nor inherited (possible typo?)",
newTokens[i].substr(1).c_str(), name.c_str(), sourceIdentifier ? (L" in '" + std::wstring(sourceIdentifier) + L"'").c_str() : L"");
}
else
{
@ -153,7 +153,7 @@ void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element)
// Recurse through the element's children
XERO_ITER_EL(element, child)
{
node.ApplyLayer(xmb, child);
node.ApplyLayer(xmb, child, sourceIdentifier);
}
// Add the element's attributes, prefixing names with "@"

View File

@ -123,8 +123,11 @@ public:
* Loads the XML data specified by @a file into the node @a ret.
* Any existing data in @a ret will be overwritten or else kept, so this
* can be called multiple times to build up a node from multiple inputs.
*
* @param sourceIdentifier Optional; string you can pass along to indicate the source of
* the data getting loaded. Used for output to log messages if an error occurs.
*/
static void LoadXML(CParamNode& ret, const XMBFile& file);
static void LoadXML(CParamNode& ret, const XMBFile& file, const wchar_t* sourceIdentifier = NULL);
/**
* Loads the XML data specified by @a path into the node @a ret.
@ -136,8 +139,11 @@ public:
/**
* See LoadXML, but parses the XML string @a xml.
* @return error code if parsing failed, else @c PSRETURN_OK
*
* @param sourceIdentifier Optional; string you can pass along to indicate the source of
* the data getting loaded. Used for output to log messages if an error occurs.
*/
static PSRETURN LoadXMLString(CParamNode& ret, const char* xml);
static PSRETURN LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier = NULL);
/**
* Finds the childs named @a name from @a src and from @a this, and copies the source child's children
@ -228,7 +234,16 @@ public:
static std::wstring EscapeXMLString(const std::wstring& str);
private:
void ApplyLayer(const XMBFile& xmb, const XMBElement& element);
/**
* Overlays the specified data onto this node. See class documentation for the concept and examples.
*
* @param xmb Representation of the XMB file containing an element with the data to apply.
* @param element Element inside the specified @p xmb file containing the data to apply.
* @param sourceIdentifier Optional; string you can pass along to indicate the source of
* the data getting applied. Used for output to log messages if an error occurs.
*/
void ApplyLayer(const XMBFile& xmb, const XMBElement& element, const wchar_t* sourceIdentifier = NULL);
void ResetScriptVal();

View File

@ -488,7 +488,7 @@ QUERYHANDLER(PickSimilarObjects)
if (cmpOwnership)
owner = cmpOwnership->GetOwner();
msg->ids = EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, owner, false, true, true);
msg->ids = EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, owner, false, true, true, false);
}