Index: ps/trunk/binaries/data/mods/public/gui/session/selection.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 15612)
@@ -1,530 +1,542 @@
// Limits selection size
const MAX_SELECTION_SIZE = 200;
// Alpha value of hovered/mouseover/highlighted selection overlays
// (should probably be greater than always visible alpha value,
// see CCmpSelectable)
const HIGHLIGHTED_ALPHA = 0.75;
function _setHighlight(ents, alpha, selected)
{
if (ents.length)
Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities":ents, "alpha":alpha, "selected":selected });
}
function _setStatusBars(ents, enabled)
{
if (ents.length)
Engine.GuiInterfaceCall("SetStatusBars", { "entities":ents, "enabled":enabled });
}
function _setMotionOverlay(ents, enabled)
{
if (ents.length)
Engine.GuiInterfaceCall("SetMotionDebugOverlay", { "entities":ents, "enabled":enabled });
}
function _playSound(ent)
{
Engine.GuiInterfaceCall("PlaySound", { "name":"select", "entity":ent });
}
/**
* EntityGroups class for managing grouped entities
*/
function EntityGroups()
{
this.groups = {};
this.ents = {};
}
EntityGroups.prototype.reset = function()
{
this.groups = {};
this.ents = {};
};
EntityGroups.prototype.add = function(ents)
{
for each (var ent in ents)
{
if (!this.ents[ent])
{
var entState = GetEntityState(ent);
// When this function is called during group rebuild, deleted
// entities will not yet have been removed, so entities might
// still be present in the group despite not existing.
if (!entState)
continue;
var templateName = entState.template;
var key = GetTemplateData(templateName).selectionGroupName || templateName;
// TODO ugly hack, just group them by player too.
// Prefix garrisoned unit's selection name with the player they belong to
var index = templateName.indexOf("&");
if (index != -1 && key.indexOf("&") == -1)
key = templateName.slice(0, index+1) + key;
if (this.groups[key])
this.groups[key] += 1;
else
this.groups[key] = 1;
this.ents[ent] = key;
}
}
};
EntityGroups.prototype.removeEnt = function(ent)
{
var templateName = this.ents[ent];
// Remove the entity
delete this.ents[ent];
this.groups[templateName]--;
// Remove the entire group
if (this.groups[templateName] == 0)
delete this.groups[templateName];
};
EntityGroups.prototype.rebuildGroup = function(renamed)
{
var oldGroup = this.ents;
this.reset();
var toAdd = [];
for (var ent in oldGroup)
toAdd.push(renamed[ent] ? renamed[ent] : +ent);
this.add(toAdd);
}
EntityGroups.prototype.getCount = function(templateName)
{
return this.groups[templateName];
};
EntityGroups.prototype.getTotalCount = function()
{
var totalCount = 0;
for each (var group in this.groups)
{
totalCount += group;
}
return totalCount;
};
EntityGroups.prototype.getTemplateNames = function()
{
var templateNames = [];
for (var templateName in this.groups)
templateNames.push(templateName);
//Preserve order even when shuffling units around
//Can be optimized by moving the sorting elsewhere
templateNames.sort();
return templateNames;
};
EntityGroups.prototype.getEntsByName = function(templateName)
{
var ents = [];
for (var ent in this.ents)
{
if (this.ents[ent] == templateName)
ents.push(+ent);
}
return ents;
};
/**
* get a list of entities grouped by templateName
*/
EntityGroups.prototype.getEntsGrouped = function()
{
var templateNames = this.getTemplateNames();
var list = [];
for (var t of templateNames)
{
list.push({
"ents": this.getEntsByName(t),
"template": t,
});
}
return list;
};
/**
* Gets all ents in every group except ones of the specified group
*/
EntityGroups.prototype.getEntsByNameInverse = function(templateName)
{
var ents = [];
for (var ent in this.ents)
{
if (this.ents[ent] != templateName)
ents.push(+ent);
}
return ents;
};
/**
* EntitySelection class for managing the entity selection list and the primary selection
*/
function EntitySelection()
{
// Private properties:
//--------------------------------
this.selected = {}; // { id:id, id:id, ... } for each selected entity ID 'id'
// { id:id, ... } for mouseover-highlighted entity IDs in these, the key is a string and the value is an int;
// we want to use the int form wherever possible since it's more efficient to send to the simulation code)
this.highlighted = {};
this.motionDebugOverlay = false;
// Public properties:
//--------------------------------
this.dirty = false; // set whenever the selection has changed
this.groups = new EntityGroups();
}
/**
* Deselect everything but entities of the chosen type if the modifier is true otherwise deselect just the chosen entity
*/
EntitySelection.prototype.makePrimarySelection = function(templateName, modifierKey)
{
var selection = this.toList();
var template = GetTemplateData(templateName);
var key = template.selectionGroupName || templateName;
var ents = [];
if (modifierKey)
ents = this.groups.getEntsByNameInverse(key);
else
ents = this.groups.getEntsByName(key);
this.reset();
this.addList(ents);
}
/**
* Get a list of the template names
*/
EntitySelection.prototype.getTemplateNames = function()
{
var templateNames = [];
var ents = this.toList();
for each (var ent in ents)
{
var entState = GetEntityState(ent);
if (entState)
templateNames.push(entState.template);
}
return templateNames;
}
/**
* Update the selection to take care of changes (like units that have been killed)
*/
EntitySelection.prototype.update = function()
{
- var changed = false;
this.checkRenamedEntities();
+
+ var miraged = {};
+ var changed = false;
for each (var ent in this.selected)
{
var entState = GetEntityState(ent);
// Remove deleted units
if (!entState)
{
delete this.selected[ent];
this.groups.removeEnt(ent);
changed = true;
continue;
}
+ // Manually replace newly miraged entities by their mirages
+ if (entState.fogging && entState.fogging.mirage)
+ {
+ miraged[ent] = entState.fogging.mirage;
+ continue;
+ }
+
// Remove non-visible units (e.g. moved back into fog-of-war)
if (entState.visibility == "hidden")
{
// Disable any highlighting of the disappeared unit
_setHighlight([ent], 0, false);
_setStatusBars([ent], false);
_setMotionOverlay([ent], false);
delete this.selected[ent];
this.groups.removeEnt(ent);
changed = true;
continue;
}
}
+
+ this.rebuildSelection(miraged);
+
if (changed)
this.onChange();
};
/**
* Update selection if some selected entities were renamed
* (in case of unit promotion or finishing building structure)
*/
EntitySelection.prototype.checkRenamedEntities = function()
{
var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities");
if (renamedEntities.length > 0)
{
var renamedLookup = {};
for each (var renamedEntity in renamedEntities)
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
// Reconstruct the selection if at least one entity has been renamed.
for each (var renamedEntity in renamedEntities)
{
if (this.selected[renamedEntity.entity])
{
this.rebuildSelection(renamedLookup);
break;
}
}
}
}
/**
* Add entities to selection. Play selection sound unless quiet is true
*/
EntitySelection.prototype.addList = function(ents, quiet)
{
var selection = this.toList();
var playerID = Engine.GetPlayerID();
// If someone else's player is the sole selected unit, don't allow adding to the selection
if (!g_DevSettings.controlAll && selection.length == 1)
{
var firstEntState = GetEntityState(selection[0]);
if (firstEntState && firstEntState.player != playerID)
return;
}
// Allow selecting things not belong to this player (enemy, ally, gaia)
var allowUnownedSelect = g_DevSettings.controlAll || (ents.length == 1 && selection.length == 0);
var i = 1;
var added = [];
for each (var ent in ents)
{
// Only add entities we own to our selection
var entState = GetEntityState(ent);
if (!this.selected[ent] && (selection.length + i) <= MAX_SELECTION_SIZE && (allowUnownedSelect || (entState && entState.player == playerID)))
{
added.push(ent);
this.selected[ent] = ent;
i++;
}
}
_setHighlight(added, 1, true);
_setStatusBars(added, true);
_setMotionOverlay(added, this.motionDebugOverlay);
if (added.length)
{
// Play the sound if the entity is controllable by us or Gaia-owned.
var owner = GetEntityState(added[0]).player;
if (!quiet && (owner == playerID || owner == 0 || g_DevSettings.controlAll))
_playSound(added[0]);
}
this.groups.add(this.toList()); // Create Selection Groups
this.onChange();
};
EntitySelection.prototype.removeList = function(ents)
{
var removed = [];
for each (var ent in ents)
{
if (this.selected[ent])
{
this.groups.removeEnt(ent);
removed.push(ent);
delete this.selected[ent];
}
}
_setHighlight(removed, 0, false);
_setStatusBars(removed, false);
_setMotionOverlay(removed, false);
this.onChange();
};
EntitySelection.prototype.reset = function()
{
_setHighlight(this.toList(), 0, false);
_setStatusBars(this.toList(), false);
_setMotionOverlay(this.toList(), false);
this.selected = {};
this.groups.reset();
this.onChange();
};
EntitySelection.prototype.rebuildSelection = function(renamed)
{
var oldSelection = this.selected;
this.reset();
var toAdd = [];
for each (var ent in oldSelection)
toAdd.push(renamed[ent] ? renamed[ent] : ent);
this.addList(toAdd, true); // don't play selection sounds
}
EntitySelection.prototype.toList = function(){
var ents = [];
for each (var ent in this.selected)
ents.push(ent);
return ents;
};
EntitySelection.prototype.setHighlightList = function(ents)
{
var highlighted = {};
for each (var ent in ents)
highlighted[ent] = ent;
var removed = [];
var added = [];
// Remove highlighting for the old units that are no longer highlighted
// (excluding ones that are actively selected too)
for each (var ent in this.highlighted)
if (!highlighted[ent] && !this.selected[ent])
removed.push(+ent);
// Add new highlighting for units that aren't already highlighted
for each (var ent in ents)
if (!this.highlighted[ent] && !this.selected[ent])
added.push(+ent);
_setHighlight(removed, 0, false);
_setStatusBars(removed, false);
_setHighlight(added, HIGHLIGHTED_ALPHA, true);
_setStatusBars(added, true);
// Store the new highlight list
this.highlighted = highlighted;
};
EntitySelection.prototype.SetMotionDebugOverlay = function(enabled)
{
this.motionDebugOverlay = enabled;
_setMotionOverlay(this.toList(), enabled);
};
EntitySelection.prototype.onChange = function()
{
this.dirty = true;
if (this == g_Selection)
onSelectionChange();
}
/**
* Cache some quantities which depends only on selection
*/
var g_Selection = new EntitySelection();
var g_canMoveIntoFormation = {};
var g_allBuildableEntities = undefined;
var g_allTrainableEntities = undefined;
// Reset cached quantities
function onSelectionChange()
{
g_canMoveIntoFormation = {};
g_allBuildableEntities = undefined;
g_allTrainableEntities = undefined;
}
/**
* EntityGroupsContainer class for managing grouped entities
*/
function EntityGroupsContainer()
{
this.groups = [];
for (var i = 0; i < 10; ++i)
{
this.groups[i] = new EntityGroups();
}
}
EntityGroupsContainer.prototype.addEntities = function(groupName, ents)
{
for each (var ent in ents)
{
for each (var group in this.groups)
{
if (ent in group.ents)
{
group.removeEnt(ent);
}
}
}
this.groups[groupName].add(ents);
}
EntityGroupsContainer.prototype.update = function()
{
this.checkRenamedEntities();
for each (var group in this.groups)
{
for (var ent in group.ents)
{
var entState = GetEntityState(+ent);
// Remove deleted units
if (!entState)
{
group.removeEnt(ent);
}
}
}
}
/**
* Update control group if some entities in the group were renamed
* (in case of unit promotion or finishing building structure)
*/
EntityGroupsContainer.prototype.checkRenamedEntities = function()
{
var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities");
if (renamedEntities.length > 0)
{
var renamedLookup = {};
for each (var renamedEntity in renamedEntities)
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
for each (var group in this.groups)
{
for each (var renamedEntity in renamedEntities)
{
// Reconstruct the group if at least one entity has been renamed.
if (renamedEntity.entity in group.ents)
{
group.rebuildGroup(renamedLookup);
break;
}
}
}
}
}
var g_Groups = new EntityGroupsContainer();
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 15612)
@@ -1,432 +1,432 @@
function layoutSelectionSingle()
{
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
function layoutSelectionMultiple()
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
function getResourceTypeDisplayName(resourceType)
{
var resourceCode = resourceType["generic"];
var displayName = "";
if (resourceCode == "treasure")
displayName = getLocalizedResourceName(resourceType["specific"], "firstWord");
else
displayName = getLocalizedResourceName(resourceCode, "firstWord");
return displayName;
}
// Fills out information that most entities have
function displaySingle(entState, template)
{
// Get general unit and player data
var specificName = template.name.specific;
var genericName = template.name.generic != template.name.specific ? template.name.generic : "";
// If packed, add that to the generic name (reduces template clutter)
if (genericName && template.pack && template.pack.state == "packed")
genericName = sprintf(translate("%(genericName)s — Packed"), { genericName: genericName });
var playerState = g_Players[entState.player];
var civName = g_CivData[playerState.civ].Name;
var civEmblem = g_CivData[playerState.civ].Emblem;
var playerName = playerState.name;
var playerColor = playerState.color.r + " " + playerState.color.g + " " + playerState.color.b + " 128";
// Indicate disconnected players by prefixing their name
if (g_Players[entState.player].offline)
{
playerName = sprintf(translate("[OFFLINE] %(player)s"), { player: playerName });
}
// Rank
if (entState.identity && entState.identity.rank && entState.identity.classes)
{
Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), { rank: translateWithContext("Rank", entState.identity.rank) });
Engine.GetGUIObjectByName("rankIcon").sprite = getRankIconSprite(entState);
Engine.GetGUIObjectByName("rankIcon").hidden = false;
}
else
{
Engine.GetGUIObjectByName("rankIcon").hidden = true;
Engine.GetGUIObjectByName("rankIcon").tooltip = "";
}
// Hitpoints
if (entState.hitpoints)
{
var unitHealthBar = Engine.GetGUIObjectByName("healthBar");
var healthSize = unitHealthBar.size;
healthSize.rright = 100*Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints));
unitHealthBar.size = healthSize;
Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), {
hitpoints: Math.ceil(entState.hitpoints),
maxHitpoints: entState.maxHitpoints
});
Engine.GetGUIObjectByName("healthSection").hidden = false;
}
else
{
Engine.GetGUIObjectByName("healthSection").hidden = true;
}
// TODO: Stamina
var player = Engine.GetPlayerID();
if (entState.stamina && (entState.player == player || g_DevSettings.controlAll))
Engine.GetGUIObjectByName("staminaSection").hidden = false;
else
Engine.GetGUIObjectByName("staminaSection").hidden = true;
// Experience
if (entState.promotion)
{
var experienceBar = Engine.GetGUIObjectByName("experienceBar");
var experienceSize = experienceBar.size;
experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req)));
experienceBar.size = experienceSize;
if (entState.promotion.curr < entState.promotion.req)
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s / %(required)s"), {
experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
current: Math.floor(entState.promotion.curr),
required: entState.promotion.req
});
else
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s"), {
experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
current: Math.floor(entState.promotion.curr)
});
Engine.GetGUIObjectByName("experience").hidden = false;
}
else
{
Engine.GetGUIObjectByName("experience").hidden = true;
}
// Resource stats
if (entState.resourceSupply)
{
var resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol
sprintf(translate("%(amount)s / %(max)s"), { amount: Math.ceil(+entState.resourceSupply.amount), max: entState.resourceSupply.max });
var resourceType = getResourceTypeDisplayName(entState.resourceSupply.type);
var unitResourceBar = Engine.GetGUIObjectByName("resourceBar");
var resourceSize = unitResourceBar.size;
resourceSize.rright = entState.resourceSupply.isInfinite ? 100 :
100 * Math.max(0, Math.min(1, +entState.resourceSupply.amount / +entState.resourceSupply.max));
unitResourceBar.size = resourceSize;
Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), { resource: resourceType });
Engine.GetGUIObjectByName("resourceStats").caption = resources;
if (entState.hitpoints)
Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("staminaSection").size;
else
Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("healthSection").size;
Engine.GetGUIObjectByName("resourceSection").hidden = false;
}
else
{
Engine.GetGUIObjectByName("resourceSection").hidden = true;
}
// Resource carrying
if (entState.resourceCarrying && entState.resourceCarrying.length)
{
// We should only be carrying one resource type at once, so just display the first
var carried = entState.resourceCarrying[0];
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+carried.type+".png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { amount: carried.amount, max: carried.max });
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = "";
}
// Use the same indicators for traders
else if (entState.trader && entState.trader.goods.amount)
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+entState.trader.goods.type+".png";
var totalGain = entState.trader.goods.amount.traderGain;
if (entState.trader.goods.amount.market1Gain)
totalGain += entState.trader.goods.amount.market1Gain;
if (entState.trader.goods.amount.market2Gain)
totalGain += entState.trader.goods.amount.market2Gain;
Engine.GetGUIObjectByName("resourceCarryingText").caption = totalGain;
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Gain: %(amount)s"), { amount: getTradingTooltip(entState.trader.goods.amount) });
}
// And for number of workers
- else if (entState.foundation)
+ else if (entState.foundation && !entState.mirage)
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.foundation.numBuilders + " ";
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Number of builders.\nTasking another to this foundation would speed construction up by %(numb)s%%"), { numb : Math.round((Math.pow((entState.foundation.numBuilders+1)/entState.foundation.numBuilders, 0.7) - 1.0)*100) });
}
- else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints))
+ else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints) && !entState.mirage)
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { amount: entState.resourceSupply.gatherers.length, max: entState.resourceSupply.maxGatherers }) + " ";
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Current/max gatherers");
}
else
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = true;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = true;
}
// Set Player details
Engine.GetGUIObjectByName("specific").caption = specificName;
Engine.GetGUIObjectByName("player").caption = playerName;
Engine.GetGUIObjectByName("playerColorBackground").sprite = "colour: " + playerColor;
if (genericName)
{
Engine.GetGUIObjectByName("generic").caption = sprintf(translate("(%(genericName)s)"), { genericName: genericName });
}
else
{
Engine.GetGUIObjectByName("generic").caption = "";
}
if ("gaia" != playerState.civ)
{
Engine.GetGUIObjectByName("playerCivIcon").sprite = "stretched:grayscale:" + civEmblem;
Engine.GetGUIObjectByName("player").tooltip = civName;
}
else
{
Engine.GetGUIObjectByName("playerCivIcon").sprite = "";
Engine.GetGUIObjectByName("player").tooltip = "";
}
// Icon image
if (template.icon)
{
Engine.GetGUIObjectByName("icon").sprite = "stretched:session/portraits/" + template.icon;
}
else
{
// TODO: we should require all entities to have icons, so this case never occurs
Engine.GetGUIObjectByName("icon").sprite = "bkFillBlack";
}
var armorLabel = "[font=\"sans-bold-13\"]" + translate("Armor:") + "[/font]"
var armorString = sprintf(translate("%(label)s %(details)s"), { label: armorLabel, details: armorTypeDetails(entState.armour) });
// Attack and Armor
if ("attack" in entState && entState.attack)
{
// Rate
if (entState.buildingAI)
var rateLabel = "[font=\"sans-bold-13\"]" + translate("Interval:") + "[/font]";
else
var rateLabel = "[font=\"sans-bold-13\"]" + translate("Rate:") + "[/font]";
var rate = sprintf(translate("%(label)s %(details)s"), {
label: rateLabel,
details: attackRateDetails(entState)
});
var attack;
var label = "[font=\"sans-bold-13\"]" + getAttackTypeLabel(entState.attack.type) + "[/font]"
if (entState.attack.type == "Ranged")
{
var realRange = entState.attack.elevationAdaptedRange;
var range = entState.attack.maxRange;
var rangeLabel = "[font=\"sans-bold-13\"]" + translate("Range:") + "[/font]"
var relativeRange = Math.round((realRange - range));
var meters = "[font=\"sans-10\"][color=\"orange\"]" + translate("meters") + "[/color][/font]";
if (relativeRange > 0)
attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s (%(relative)s), %(rate)s"), {
label: label,
details: damageTypeDetails(entState.attack),
rangeLabel: rangeLabel,
range: Math.round(range),
meters: meters,
relative: "+" + relativeRange,
rate: rate
});
else if (relativeRange < 0)
attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s (%(relative)s), %(rate)s"), {
label: label,
details: damageTypeDetails(entState.attack),
rangeLabel: rangeLabel,
range: Math.round(range),
meters: meters,
relative: relativeRange,
rate: rate
});
else // don't show when it's 0
attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s, %(rate)s"), {
label: label,
details: damageTypeDetails(entState.attack),
rangeLabel: rangeLabel,
range: Math.round(range),
meters: meters,
rate: rate
});
}
else
{
attack = sprintf(translate("%(label)s %(details)s, %(rate)s"), {
label: label,
details: damageTypeDetails(entState.attack),
rate: rate
});
}
Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n" + armorString;
}
else
{
Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = armorString;
}
// Icon Tooltip
var iconTooltip = "";
if (genericName)
iconTooltip = "[font=\"sans-bold-16\"]" + genericName + "[/font]";
if (template.visibleIdentityClasses && template.visibleIdentityClasses.length)
{
iconTooltip += "\n[font=\"sans-bold-13\"]" + translate("Classes:") + "[/font] ";
iconTooltip += "[font=\"sans-13\"]" + translate(template.visibleIdentityClasses[0]) ;
for (var i = 1; i < template.visibleIdentityClasses.length; i++)
iconTooltip += ", " + translate(template.visibleIdentityClasses[i]);
iconTooltip += "[/font]";
}
if (template.auras)
{
for (var auraName in template.auras)
{
iconTooltip += "\n[font=\"sans-bold-13\"]" + translate(auraName) + "[/font]";
if (template.auras[auraName])
iconTooltip += ": " + translate(template.auras[auraName]);
}
}
if (template.tooltip)
iconTooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]";
Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltip;
// Unhide Details Area
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
// Fills out information for multiple entities
function displayMultiple(selection, template)
{
var averageHealth = 0;
var maxHealth = 0;
for (var i = 0; i < selection.length; i++)
{
var entState = GetEntityState(selection[i])
if (entState)
{
if (entState.hitpoints)
{
averageHealth += entState.hitpoints;
maxHealth += entState.maxHitpoints;
}
}
}
if (averageHealth > 0)
{
var unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple");
var healthSize = unitHealthBar.size;
healthSize.rtop = 100-100*Math.max(0, Math.min(1, averageHealth / maxHealth));
unitHealthBar.size = healthSize;
var hitpointsLabel = "[font=\"sans-bold-13\"]" + translate("Hitpoints:") + "[/font]"
var hitpoints = sprintf(translate("%(label)s %(current)s / %(max)s"), { label: hitpointsLabel, current: averageHealth, max: maxHealth });
var healthMultiple = Engine.GetGUIObjectByName("healthMultiple");
healthMultiple.tooltip = hitpoints;
healthMultiple.hidden = false;
}
else
{
Engine.GetGUIObjectByName("healthMultiple").hidden = true;
}
// TODO: Stamina
// Engine.GetGUIObjectByName("staminaBarMultiple");
Engine.GetGUIObjectByName("numberOfUnits").caption = selection.length;
// Unhide Details Area
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
// Updates middle entity Selection Details Panel
function updateSelectionDetails()
{
var supplementalDetailsPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails");
var detailsPanel = Engine.GetGUIObjectByName("selectionDetails");
var commandsPanel = Engine.GetGUIObjectByName("unitCommands");
g_Selection.update();
var selection = g_Selection.toList();
if (selection.length == 0)
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
hideUnitCommands();
supplementalDetailsPanel.hidden = true;
detailsPanel.hidden = true;
commandsPanel.hidden = true;
return;
}
/* If the unit has no data (e.g. it was killed), don't try displaying any
data for it. (TODO: it should probably be removed from the selection too;
also need to handle multi-unit selections) */
var entState = GetExtendedEntityState(selection[0]);
if (!entState)
return;
var template = GetTemplateData(entState.template);
// Fill out general info and display it
if (selection.length == 1)
displaySingle(entState, template);
else
displayMultiple(selection, template);
// Show basic details.
detailsPanel.hidden = false;
if (g_IsObserver)
{
// Observers don't need these displayed.
supplementalDetailsPanel.hidden = true;
commandsPanel.hidden = true;
}
else
{
// Fill out commands panel for specific unit selected (or first unit of primary group)
updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection);
}
}
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 15612)
@@ -1,885 +1,894 @@
/**
* List of different actions units can execute,
* this is mostly used to determine which actions can be executed
*
* "execute" is meant to send the command to the engine
*
* The next functions will always return false
* in case you have to continue to seek
* (i.e. look at the next entity for getActionInfo, the next
* possible action for the actionCheck ...)
* They will return an object when the searching is finished
*
* "getActionInfo" is used to determine if the action is possible,
* and also give visual feedback to the user (tooltips, cursors, ...)
*
* "preSelectedActionCheck" is used to select actions when the gui buttons
* were used to set them, but still require a target (like the guard button)
*
* "hotkeyActionCheck" is used to check the possibility of actions when
* a hotkey is pressed
*
* "actionCheck" is used to check the possibilty of actions without specific
* command. For that, the specificness variable is used
*
* "specificness" is used to determine how specific an action is,
* The lower the number, the more specific an action is, and the bigger
* the chance of selecting that action when multiple actions are possible
*/
var unitActions =
{
"move":
{
"execute": function(target, action, selection, queued)
{
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;
},
"getActionInfo": function(entState, targetState)
{
return {"possible": true};
},
"actionCheck": function(target, selection)
{
// Work out whether at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
if (haveUnitAI && getActionInfo("move", target).possible)
return {"type": "move"};
return false;
},
"specificness": 12,
},
"attack-move":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
return {"possible": true};
},
"hotkeyActionCheck": function(target, selection)
{
// Work out whether at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
if (haveUnitAI && Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible)
return {"type": "attack-move", "cursor": "action-attack-move"};
return false;
},
"specificness": 30,
},
"attack":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState.hitpoints)
return false;
if (playerCheck(entState, targetState, ["Neutral", "Enemy"]))
return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})};
return false;
},
"hotkeyActionCheck": function(target)
{
if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible)
return {"type": "attack", "cursor": "action-attack", "target": target};
return false
},
"actionCheck": function(target)
{
if (getActionInfo("attack", target).possible)
return {"type": "attack", "cursor": "action-attack", "target": target};
return false;
},
"specificness": 10,
},
"heal":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.healer)
return false;
if (!hasClass(targetState, "Unit") || !targetState.needsHeal)
return false;
if (!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
// Healers can't heal themselves.
if (entState.id == targetState.id)
return false;
var unhealableClasses = entState.healer.unhealableClasses;
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
return false;
var healableClasses = entState.healer.healableClasses;
if (!MatchesClassList(targetState.identity.classes, healableClasses))
return false;
return {"possible": true};
},
"actionCheck": function(target)
{
if (getActionInfo("heal", target).possible)
return {"type": "heal", "cursor": "action-heal", "target": target};
return false;
},
"specificness": 7,
},
"build":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (targetState.foundation && entState.buildEntities && playerCheck(entState, targetState, ["Player", "Ally"]))
return {"possible": true};
return false;
},
"actionCheck": function(target)
{
if (getActionInfo("build", target).possible)
return {"type": "build", "cursor": "action-build", "target": target};
return false;
},
"specificness": 3,
},
"repair":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (entState.buildEntities && targetState.needsRepair && playerCheck(entState, targetState, ["Player", "Ally"]))
return {"possible": true};
return false;
},
"preSelectedActionCheck" : function(target)
{
if (preSelectedAction != ACTION_REPAIR)
return false;
if (getActionInfo("repair", target).possible)
return {"type": "repair", "cursor": "action-repair", "target": target};
return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
},
"actionCheck": function(target)
{
if (getActionInfo("repair", target).possible)
return {"type": "build", "cursor": "action-repair", "target": target};
return false;
},
"specificness": 4,
},
"gather":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceSupply)
return false;
var resource = findGatherType(entState, targetState.resourceSupply);
if (resource)
return {"possible": true, "cursor": "action-gather-" + resource};
return false;
},
"actionCheck": function(target)
{
var actionInfo = getActionInfo("gather", target);
if (!actionInfo.possible)
return false;
return {"type": "gather", "cursor": actionInfo.cursor, "target": target};
},
"specificness": 1,
},
"returnresource":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceDropsite)
return false;
if (!playerCheck(entState, targetState, ["Player"]))
return false;
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
return false;
var carriedType = entState.resourceCarrying[0].type;
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
return false;
return {"possible": true, "cursor": "action-return-" + carriedType};
},
"actionCheck": function(target)
{
var actionInfo = getActionInfo("returnresource", target);
if (!actionInfo.possible)
return false;
return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target};
},
"specificness": 2,
},
"setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target, "source": undefined, "route": undefined, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (targetState.foundation || !entState.trader)
return false;
if (!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
if (!(hasClass(entState, "Organic") && hasClass(targetState, "Market")) && !(hasClass(entState, "Ship") && hasClass(targetState, "NavalMarket")))
return false;
var tradingData = {"trader": entState.id, "target": targetState.id};
var tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", tradingData);
if (!tradingDetails)
return false;
var tooltip;
switch (tradingDetails.type)
{
case "is first":
tooltip = translate("Origin trade market.");
if (tradingDetails.hasBothMarkets)
tooltip += "\n" + sprintf(translate("Gain: %(gain)s"), {
gain: getTradingTooltip(tradingDetails.gain)
});
else
tooltip += "\n" + translate("Right-click on another market to set it as a destination trade market.")
break;
case "is second":
tooltip = translate("Destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), {
gain: getTradingTooltip(tradingDetails.gain)
});
break;
case "set first":
tooltip = translate("Right-click to set as origin trade market");
break;
case "set second":
if (tradingDetails.gain.traderGain == 0) // markets too close
return false;
tooltip = translate("Right-click to set as destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), {
gain: getTradingTooltip(tradingDetails.gain)
});
break;
}
return {"possible": true, "tooltip": tooltip};
},
"actionCheck": function(target)
{
var actionInfo = getActionInfo("setup-trade-route", target);
if (!actionInfo.possible)
return false;
return {"type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target};
},
"specificness": 0,
},
"garrison":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!hasClass(entState, "Unit") || !targetState.garrisonHolder)
return false;
if (!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
var tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount,
capacity: targetState.garrisonHolder.capacity
});
var extraCount = 0;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.garrisonedEntitiesCount;
if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
return {"possible": true, "tooltip": tooltip};
return false;
},
"preSelectedActionCheck": function(target)
{
if (preSelectedAction != ACTION_GARRISON)
return false;
var actionInfo = getActionInfo("garrison", target);
if (actionInfo.possible)
return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target};
return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined};
},
"hotkeyActionCheck": function(target)
{
var actionInfo = getActionInfo("garrison", target);
if (Engine.HotkeyIsPressed("session.garrison") && actionInfo.possible)
return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target};
return false;
},
"specificness": 20,
},
"guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "guard", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.guard)
return false;
if (!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
if (!entState.unitAI || !entState.unitAI.canGuard)
return false;
if (targetState.unitAI && targetState.unitAI.isGuarding)
return false;
return {"possible": true};
},
"preSelectedActionCheck" : function(target)
{
if (preSelectedAction != ACTION_GUARD)
return false;
if (getActionInfo("guard", target).possible)
return {"type": "guard", "cursor": "action-guard", "target": target};
return {"type": "none", "cursor": "action-guard-disabled", "target": undefined};
},
"hotkeyActionCheck": function(target)
{
if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("guard", target).possible)
return {"type": "guard", "cursor": "action-guard", "target": target};
return false;
},
"specificness": 40,
},
"remove-guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "remove-guard", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] });
return true;
},
"hotkeyActionCheck": function(target, selection)
{
if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("remove-guard", target).possible)
{
var isGuarding = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI && entState.unitAI.isGuarding;
});
if (isGuarding)
return {"type": "remove-guard", "cursor": "action-remove-guard"};
}
return false;
},
"specificness": 41,
},
"set-rallypoint":
{
"execute": function(target, action, selection, queued)
{
// if there is a position set in the action then use this so that when setting a
// rally point on an entity it is centered on that entity
if (action.position)
target = action.position;
Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z, "data": action.data, "queued": queued});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
var tooltip;
// default to walking there (or attack-walking if hotkey pressed)
var data = {command: "walk"};
var cursor = "";
if (Engine.HotkeyIsPressed("session.attackmove"))
{
data = {command: "attack-walk"};
cursor = "action-attack-move";
}
if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "Ally"]))
{
data.command = "garrison";
data.target = targetState.id;
cursor = "action-garrison";
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount,
capacity: targetState.garrisonHolder.capacity
});
if (targetState.garrisonHolder.garrisonedEntitiesCount >= targetState.garrisonHolder.capacity)
tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
}
else if (targetState.resourceSupply)
{
var resourceType = targetState.resourceSupply.type;
if (resourceType.generic == "treasure")
cursor = "action-gather-" + resourceType.generic;
else
cursor = "action-gather-" + resourceType.specific;
data.command = "gather";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
}
else if (targetState.foundation && entState.buildEntities)
{
data.command = "build";
data.target = targetState.id;
cursor = "action-build";
}
else if (hasClass(entState, "Market") && hasClass(targetState, "Market") && entState.id != targetState.id &&
(!hasClass(entState, "NavalMarket") || hasClass(targetState, "NavalMarket")) && !playerCheck(entState, targetState, ["Enemy"]))
{
// Find a trader (if any) that this building can produce.
var trader;
if (entState.production && entState.production.entities.length)
for (var i = 0; i < entState.production.entities.length; ++i)
if ((trader = GetTemplateData(entState.production.entities[i]).trader))
break;
var traderData = { "firstMarket": entState.id, "secondMarket": targetState.id, "template": trader };
var gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
if (gain && gain.traderGain)
{
data.command = "trade";
data.target = traderData.secondMarket;
data.source = traderData.firstMarket;
cursor = "action-setup-trade-route";
tooltip = translate("Right-click to establish a default route for new traders.");
if (trader)
tooltip += "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(gain) });
else // Foundation or cannot produce traders
tooltip += "\n" + sprintf(translate("Expected gain: %(gain)s"), { gain: getTradingTooltip(gain) });
}
}
else if (targetState.needsRepair && playerCheck(entState, targetState, ["Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities
var selection = g_Selection.toList();
if (!Engine.HotkeyIsPressed("session.autorallypoint") || !targetState.production || !targetState.production.entities.length)
{
for (var i = 0; i < selection.length; i++)
if (targetState.id === selection[i])
return false;
}
return {"possible": true, "data": data, "position": targetState.position, "cursor": cursor, "tooltip": tooltip};
},
"actionCheck": function(target, selection)
{
// Work out whether at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
if (haveUnitAI)
return false;
// Work out whether at least part the selection have rally points
// while none have UnitAI
var haveRallyPoints = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && ("rallyPoint" in entState) && entState.rallyPoint;
});
if (!haveRallyPoints)
return false;
var actionInfo = getActionInfo("set-rallypoint", target);
if (!actionInfo.possible)
return false;
return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position};
},
"specificness": 6,
},
"unset-rallypoint":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (entState.id != targetState.id)
return false;
if (!entState.rallyPoint || !entState.rallyPoint.position)
return false;
return {"possible": true};
},
"actionCheck": function(target, selection)
{
// Work out whether at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
// Work out whether at least part the selection have rally points
// while none have UnitAI
var haveRallyPoints = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && ("rallyPoint" in entState) && entState.rallyPoint;
});
if (!haveUnitAI && haveRallyPoints && getActionInfo("unset-rallypoint", target).possible)
return {"type": "unset-rallypoint", "cursor": "action-unset-rally"};
return false;
},
"specificness": 11,
},
"none":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"specificness": 100,
},
};
/**
* Info and actions for the entity commands
* Currently displayed in the bottom of the central pane
*/
var g_EntityCommands =
{
// Unload
"unload-all": {
"getInfo": function(entState)
{
if (!entState.garrisonHolder)
return false;
var selection = g_Selection.toList();
var count = 0;
for (var ent of selection)
{
var state = GetEntityState(ent);
if (state.garrisonHolder)
count += state.garrisonHolder.entities.length;
}
return {
"tooltip": translate("Unload All"),
"icon": "garrison-out.png",
"count": count,
};
},
"execute": function(entState)
{
unloadAll();
},
},
// Delete
"delete": {
"getInfo": function(entState)
{
+ if (entState.mirage)
+ return {
+ "tooltip": translate("You cannot destroy this entity because it is in the fog-of-war"),
+ "icon": "kill_small.png"
+ };
+
return {
"tooltip": translate("Delete"),
"icon": "kill_small.png"
};
},
"execute": function(entState)
{
+ if (entState.mirage)
+ return;
+
var selection = g_Selection.toList();
if (selection.length < 1)
return;
if (!entState.resourceSupply || !entState.resourceSupply.killBeforeGather)
openDeleteDialog(selection);
},
},
// Stop
"stop": {
"getInfo": function(entState)
{
if (!entState.unitAI)
return false;
return {
"tooltip": translate("Stop"),
"icon": "stop.png"
};
},
"execute": function(entState)
{
var selection = g_Selection.toList();
if (selection.length > 0)
stopUnits(selection);
},
},
// Garrison
"garrison": {
"getInfo": function(entState)
{
if (!entState.unitAI || entState.turretParent)
return false;
return {
"tooltip": translate("Garrison"),
"icon": "garrison.png"
};
},
"execute": function(entState)
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
},
},
// Ungarrison
"unload": {
"getInfo": function(entState)
{
if (!entState.unitAI || !entState.turretParent)
return false;
var p = GetEntityState(entState.turretParent);
if (!p.garrisonHolder || p.garrisonHolder.entities.indexOf(entState.id) == -1)
return false;
return {
"tooltip": translate("Unload"),
"icon": "garrison-out.png"
};
},
"execute": function(entState)
{
unloadSelection();
},
},
// Repair
"repair": {
"getInfo": function(entState)
{
if (!entState.buildEntities)
return false;
return {
"tooltip": translate("Repair"),
"icon": "repair.png"
};
},
"execute": function(entState)
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
},
},
// Focus on rally point
"focus-rally": {
"getInfo": function(entState)
{
if (!entState.rallyPoint)
return false;
return {
"tooltip": translate("Focus on Rally Point"),
"icon": "focus-rally.png"
};
},
"execute": function(entState)
{
var focusTarget = null;
if (entState.rallyPoint && entState.rallyPoint.position)
focusTarget = entState.rallyPoint.position;
else if (entState.position)
focusTarget = entState.position;
if (focusTarget)
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
},
},
// Back to work
"back-to-work": {
"getInfo": function(entState)
{
if (!entState.unitAI || !entState.unitAI.hasWorkOrders)
return false;
return {
"tooltip": translate("Back to Work"),
"icon": "production.png"
};
},
"execute": function(entState)
{
backToWork();
},
},
// Guard
"add-guard": {
"getInfo": function(entState)
{
if (!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding)
return false;
return {
"tooltip": translate("Guard"),
"icon": "add-guard.png"
};
},
"execute": function(entState)
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
},
},
// Remove guard
"remove-guard": {
"getInfo": function(entState)
{
if (!entState.unitAI || !entState.unitAI.isGuarding)
return false;
return {
"tooltip": translate("Remove guard"),
"icon": "remove-guard.png"
};
},
"execute": function(entState)
{
removeGuard();
},
},
// Trading
"select-trading-goods": {
"getInfo": function(entState)
{
if (!hasClass(entState, "Market"))
return false;
return {
"tooltip": translate("Select trading goods"),
"icon": "economics.png"
};
},
"execute": function(entState)
{
toggleTrade();
},
},
// Raise alert
"increase-alert-level": {
"getInfo": function(entState)
{
if(!entState.alertRaiser || !entState.alertRaiser.canIncreaseLevel)
return false;
if(entState.alertRaiser.hasRaisedAlert)
var tooltip = translate("Increase the alert level to protect more units");
else
var tooltip = translate("Raise an alert!");
return {
"tooltip": tooltip,
"icon": "bell_level1.png"
};
},
"execute": function(entState)
{
increaseAlertLevel();
},
},
// End alert
"alert-end": {
"getInfo": function(entState)
{
if(!entState.alertRaiser || !entState.alertRaiser.hasRaisedAlert)
return false
return {
"tooltip": translate("End of alert."),
"icon": "bell_level0.png"
};
},
"execute": function(entState)
{
endOfAlert();
},
},
};
function playerCheck(entState, targetState, validPlayers)
{
var playerState = GetSimState().players[entState.player];
for (var player of validPlayers)
{
if (player == "Gaia" && targetState.player == 0)
return true;
if (player == "Player" && targetState.player == entState.player)
return true;
if (playerState["is"+player] && playerState["is"+player][targetState.player])
return true;
}
return false;
};
Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 15612)
@@ -1,345 +1,350 @@
//Number of rounds of firing per 2 seconds
const roundCount = 10;
const attackType = "Ranged";
function BuildingAI() {}
BuildingAI.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2;
/**
* Initialize BuildingAI Component
*/
BuildingAI.prototype.Init = function()
{
if (this.GetDefaultArrowCount() > 0 || this.GetGarrisonArrowMultiplier() > 0)
{
this.currentRound = 0;
//Arrows left to fire
this.arrowsLeft = 0;
this.targetUnits = [];
}
};
BuildingAI.prototype.OnOwnershipChanged = function(msg)
{
// Remove current targets, to prevent them from being added twice
this.targetUnits = [];
if (msg.to != -1)
this.SetupRangeQuery(msg.to);
// Non-Gaia buildings should attack certain Gaia units.
if (msg.to != 0 || this.gaiaUnitsQuery)
this.SetupGaiaRangeQuery(msg.to);
};
BuildingAI.prototype.OnDiplomacyChanged = function(msg)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
{
// Remove maybe now allied/neutral units
this.targetUnits = [];
this.SetupRangeQuery(msg.player);
}
};
/**
* Cleanup on destroy
*/
BuildingAI.prototype.OnDestroy = function()
{
if (this.timer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
// Clean up range queries
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.enemyUnitsQuery)
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
if (this.gaiaUnitsQuery)
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
};
/**
* Setup the Range Query to detect units coming in & out of range
*/
BuildingAI.prototype.SetupRangeQuery = function(owner)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
if (this.enemyUnitsQuery)
{
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
this.enemyUnitsQuery = undefined;
}
var players = [];
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player);
var numPlayers = cmpPlayerManager.GetNumPlayers();
for (var i = 1; i < numPlayers; ++i)
{ // Exclude gaia, allies, and self
// TODO: How to handle neutral players - Special query to attack military only?
if (cmpPlayer.IsEnemy(i))
players.push(i);
}
var range = cmpAttack.GetRange(attackType);
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
};
// Set up a range query for Gaia units within LOS range which can be attacked.
// This should be called whenever our ownership changes.
BuildingAI.prototype.SetupGaiaRangeQuery = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
if (this.gaiaUnitsQuery)
{
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
this.gaiaUnitsQuery = undefined;
}
if (owner == -1)
return;
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
if (!cmpPlayer.IsEnemy(0))
return;
var range = cmpAttack.GetRange(attackType);
// This query is only interested in Gaia entities that can attack.
this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
};
/**
* Called when units enter or leave range
*/
BuildingAI.prototype.OnRangeUpdate = function(msg)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
if (msg.tag == this.gaiaUnitsQuery)
{
const filter = function(e) {
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return (cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()));
};
if (msg.added.length)
msg.added = msg.added.filter(filter);
// Removed entities may not have cmpUnitAI.
for (var i = 0; i < msg.removed.length; ++i)
if (this.targetUnits.indexOf(msg.removed[i]) == -1)
msg.removed.splice(i--, 1);
}
else if (msg.tag != this.enemyUnitsQuery)
return;
if (msg.added.length > 0)
{
for each (var entity in msg.added)
{
if (cmpAttack.CanAttack(entity))
this.targetUnits.push(entity);
}
}
if (msg.removed.length > 0)
{
for each (var entity in msg.removed)
{
var index = this.targetUnits.indexOf(entity);
if (index > -1)
this.targetUnits.splice(index, 1);
}
}
if (!this.targetUnits.length || this.timer)
return;
// units entered the range, prepare to shoot
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var attackTimers = cmpAttack.GetTimers(attackType);
this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows", attackTimers.prepare, attackTimers.repeat / roundCount, null);
};
BuildingAI.prototype.GetDefaultArrowCount = function()
{
var arrowCount = +this.template.DefaultArrowCount;
return ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity);
};
BuildingAI.prototype.GetGarrisonArrowMultiplier = function()
{
var arrowMult = +this.template.GarrisonArrowMultiplier;
return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity);
};
BuildingAI.prototype.GetGarrisonArrowClasses = function()
{
var string = this.template.GarrisonArrowClasses;
if (string)
return string.split(/\s+/);
return [];
};
/**
* Returns the number of arrows which needs to be fired.
* DefaultArrowCount + Garrisoned Archers(ie., any unit capable
* of shooting arrows from inside buildings)
*/
BuildingAI.prototype.GetArrowCount = function()
{
var count = this.GetDefaultArrowCount();
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
count += Math.round(cmpGarrisonHolder.GetGarrisonedArcherCount(this.GetGarrisonArrowClasses()) * this.GetGarrisonArrowMultiplier());
}
return count;
};
/**
* Fires arrows. Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range
*/
BuildingAI.prototype.FireArrows = function()
{
if (!this.targetUnits.length)
{
if (this.timer)
{
// stop the timer
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
return;
}
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var arrowsToFire = 0;
if (this.currentRound > (roundCount - 1))
{
//Reached end of rounds. Reset count
this.currentRound = 0;
}
if (this.currentRound == 0)
{
//First round. Calculate arrows to fire
this.arrowsLeft = this.GetArrowCount();
}
if (this.currentRound == (roundCount - 1))
{
//Last round. Need to fire all left-over arrows
arrowsToFire = this.arrowsLeft;
}
else
{
//Fire N arrows, 0 <= N <= Number of arrows left
arrowsToFire = Math.min(
Math.round(2*Math.random() * this.GetArrowCount()/roundCount),
this.arrowsLeft
);
}
if (arrowsToFire <= 0)
{
this.currentRound++;
return;
}
var targets = new WeightedList();
for (var i = 0; i < this.targetUnits.length; i++)
{
var target = this.targetUnits[i];
var preference = cmpAttack.GetPreference(target);
var weight = 1;
if (preference !== null && preference !== undefined)
{
// Lower preference scores indicate a higher preference so they
// should result in a higher weight.
weight = 1 + this.MAX_PREFERENCE_BONUS / (1 + preference);
}
targets.push(target, weight);
}
for (var i = 0;i < arrowsToFire;i++)
{
var selectedIndex = targets.randomIndex();
var selectedTarget = targets.itemAt(selectedIndex);
if (selectedTarget && this.CheckTargetVisible(selectedTarget))
{
cmpAttack.PerformAttack(attackType, selectedTarget);
PlaySound("attack", this.entity);
}
else
{
targets.removeAt(selectedIndex);
i--; // one extra arrow left to fire
if(targets.length() < 1)
{
this.arrowsLeft += arrowsToFire;
// no targets found in this round, save arrows and go to next round
break;
}
}
}
this.arrowsLeft -= arrowsToFire;
this.currentRound++;
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
BuildingAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ // Entities that are hidden and miraged are considered visible
+ var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
+ if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
+ return true;
+
if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden")
return false;
// Either visible directly, or visible in fog
return true;
};
Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Fogging.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Fogging.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Fogging.js (revision 15612)
@@ -0,0 +1,165 @@
+const VIS_HIDDEN = 0;
+const VIS_FOGGED = 1;
+const VIS_VISIBLE = 2;
+
+function Fogging() {}
+
+Fogging.prototype.Schema =
+ "Allows this entity to be replaced by mirages entities in the fog-of-war." +
+ "";
+
+Fogging.prototype.Init = function()
+{
+ this.mirages = [];
+ this.seen = [];
+
+ var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+ for (var player = 0; player < cmpPlayerManager.GetNumPlayers(); ++player)
+ {
+ this.mirages.push(INVALID_ENTITY);
+ this.seen.push(false);
+ }
+};
+
+Fogging.prototype.LoadMirage = function(player)
+{
+ var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ var templateName = "mirage|" + cmpTemplateManager.GetCurrentTemplateName(this.entity);
+
+ // If this is an entity without visibility (e.g. a foundation), it should be
+ // marked as seen for its owner
+ var cmpParentOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (cmpParentOwnership && cmpParentOwnership.GetOwner() == player)
+ this.seen[player] = true;
+
+ if (!this.seen[player] || this.mirages[player] != INVALID_ENTITY)
+ return;
+
+ this.mirages[player] = Engine.AddEntity(templateName);
+ var cmpMirage = Engine.QueryInterface(this.mirages[player], IID_Mirage);
+ if (!cmpMirage)
+ {
+ error("Failed to load mirage entity for template " + templateName);
+ this.mirages[player] = INVALID_ENTITY;
+ return;
+ }
+
+ // Setup basic mirage properties
+ cmpMirage.SetPlayer(player);
+ cmpMirage.SetParent(this.entity);
+
+ // Copy cmpOwnership data
+ var cmpMirageOwnership = Engine.QueryInterface(this.mirages[player], IID_Ownership);
+ if (!cmpParentOwnership || !cmpMirageOwnership)
+ {
+ error("Failed to setup the ownership data of the fogged entity " + templateName);
+ return;
+ }
+ cmpMirageOwnership.SetOwner(cmpParentOwnership.GetOwner());
+
+ // Copy cmpPosition data
+ var cmpParentPosition = Engine.QueryInterface(this.entity, IID_Position);
+ var cmpMiragePosition = Engine.QueryInterface(this.mirages[player], IID_Position);
+ if (!cmpParentPosition || !cmpMiragePosition)
+ {
+ error("Failed to setup the position data of the fogged entity " + templateName);
+ return;
+ }
+ if (!cmpParentPosition.IsInWorld())
+ return;
+ var pos = cmpParentPosition.GetPosition();
+ cmpMiragePosition.JumpTo(pos.x, pos.z);
+ var rot = cmpParentPosition.GetRotation();
+ cmpMiragePosition.SetYRotation(rot.y);
+ cmpMiragePosition.SetXZRotation(rot.x, rot.z);
+
+ // Copy cmpVisualActor data
+ var cmpParentVisualActor = Engine.QueryInterface(this.entity, IID_Visual);
+ var cmpMirageVisualActor = Engine.QueryInterface(this.mirages[player], IID_Visual);
+ if (!cmpParentVisualActor || !cmpMirageVisualActor)
+ {
+ error("Failed to setup the visual data of the fogged entity " + templateName);
+ return;
+ }
+ cmpMirageVisualActor.SetActorSeed(cmpParentVisualActor.GetActorSeed());
+
+ // Store valuable information into the mirage component (especially for the GUI)
+ var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation);
+ if (cmpFoundation)
+ cmpMirage.AddFoundation(cmpFoundation.GetBuildPercentage());
+
+ var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+ if (cmpHealth)
+ cmpMirage.AddHealth(
+ cmpHealth.GetMaxHitpoints(),
+ cmpHealth.GetHitpoints(),
+ cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
+ );
+
+ var cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply);
+ if (cmpResourceSupply)
+ cmpMirage.AddResourceSupply(
+ cmpResourceSupply.GetMaxAmount(),
+ cmpResourceSupply.GetCurrentAmount(),
+ cmpResourceSupply.GetType(),
+ cmpResourceSupply.IsInfinite()
+ );
+};
+
+Fogging.prototype.IsMiraged = function(player)
+{
+ if (player >= this.mirages.length)
+ return false;
+
+ return this.mirages[player] != INVALID_ENTITY;
+};
+
+Fogging.prototype.GetMirage = function(player)
+{
+ if (player >= this.mirages.length)
+ return INVALID_ENTITY;
+
+ return this.mirages[player];
+};
+
+Fogging.prototype.WasSeen = function(player)
+{
+ if (player >= this.seen.length)
+ return false;
+
+ return this.seen[player];
+};
+
+Fogging.prototype.OnVisibilityChanged = function(msg)
+{
+ if (msg.player >= this.mirages.length)
+ return;
+
+ if (msg.newVisibility == VIS_VISIBLE)
+ {
+ this.seen[msg.player] = true;
+
+ // Destroy mirages when we get back into LoS
+ if (this.mirages[msg.player] != INVALID_ENTITY)
+ {
+ Engine.DestroyEntity(this.mirages[msg.player]);
+ this.mirages[msg.player] = INVALID_ENTITY;
+ }
+ }
+
+ // Intermediate LoS state, meaning we must create a mirage
+ if (msg.newVisibility == VIS_FOGGED)
+ this.LoadMirage(msg.player);
+};
+
+Fogging.prototype.OnDestroy = function(msg)
+{
+ for (var player = 0; player < this.mirages.length; ++player)
+ {
+ var cmpMirage = Engine.QueryInterface(this.mirages[player], IID_Mirage);
+ if (cmpMirage)
+ cmpMirage.SetParent(INVALID_ENTITY);
+ }
+};
+
+Engine.RegisterComponentType(IID_Fogging, "Fogging", Fogging);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 15612)
@@ -1,1983 +1,2022 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised so we mustn't serialise
// its non-deterministic data. Instead just return an empty object.
return {};
};
GuiInterface.prototype.Deserialize = function(obj)
{
this.Init();
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function(player)
{
var ret = {
"players": []
};
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var n = cmpPlayerMan.GetNumPlayers();
for (var i = 0; i < n; ++i)
{
var playerEnt = cmpPlayerMan.GetPlayerByID(i);
var cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Work out what phase we are in
var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
var phase = "";
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
// store player ally/neutral/enemy data as arrays
var allies = [];
var mutualAllies = [];
var neutrals = [];
var enemies = [];
for (var j = 0; j < n; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
var playerData = {
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"colour": cmpPlayer.GetColour(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"heroes": cmpPlayer.GetHeroes(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits.GetLimits(),
"entityCounts": cmpPlayerEntityLimits.GetCounts(),
"entityLimitChangers": cmpPlayerEntityLimits.GetLimitChangers(),
"researchQueued": cmpTechnologyManager.GetQueuedResearch(),
"researchStarted": cmpTechnologyManager.GetStartedResearch(),
"researchedTechs": cmpTechnologyManager.GetResearchedTechs(),
"classCounts": cmpTechnologyManager.GetClassCounts(),
"typeCountsByClass": cmpTechnologyManager.GetTypeCountsByClass()
};
ret.players.push(playerData);
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
{
ret.circularMap = cmpRangeManager.GetLosCircular();
}
// Add timeElapsed
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
// and the game type
var cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.gameType = cmpEndGameManager.GetGameType();
return ret;
};
GuiInterface.prototype.GetExtendedSimulationState = function(player)
{
// Get basic simulation info
var ret = this.GetSimulationState();
// Add statistics to each player
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var n = cmpPlayerMan.GetNumPlayers();
for (var i = 0; i < n; ++i)
{
var playerEnt = cmpPlayerMan.GetPlayerByID(i);
var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics();
}
// Add bartering prices
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterPrices = cmpBarter.GetPrices();
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function(player)
{
this.renamedEntities = [];
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
var ret = {
"id": ent,
"template": template,
"alertRaiser": null,
"buildEntities": null,
"identity": null,
+ "fogging": null,
"foundation": null,
"garrisonHolder": null,
"gate": null,
"guard": null,
"pack": null,
"player": -1,
"position": null,
"production": null,
"rallyPoint": null,
"rotation": null,
"trader": null,
"unitAI": null,
"visibility": null,
};
+ // Used for several components
+ var cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
+
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
{
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName()
};
}
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
ret.rotation = cmpPosition.GetRotation();
}
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = Math.ceil(cmpHealth.GetHitpoints());
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
ret.needsHeal = !cmpHealth.IsUnhealable();
}
+ if (cmpMirage && cmpMirage.Health())
+ {
+ ret.hitpoints = cmpMirage.GetHitpoints();
+ ret.maxHitpoints = cmpMirage.GetMaxHitpoints();
+ ret.needsRepair = cmpMirage.NeedsRepair();
+ }
var cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
{
ret.buildEntities = cmpBuilder.GetEntitiesList();
}
var cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
{
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress(),
};
}
var cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"queue": cmpProductionQueue.GetQueue(),
};
}
var cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
{
ret.trader = {
"goods": cmpTrader.GetGoods(),
"requiredGoods": cmpTrader.GetRequiredGoods()
};
}
+ var cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
+ if (cmpFogging)
+ {
+ if (cmpFogging.IsMiraged(player))
+ ret.fogging = {"mirage": cmpFogging.GetMirage(player)};
+ else
+ ret.fogging = {"mirage": null};
+ }
+
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
{
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage(),
"numBuilders": cmpFoundation.GetNumBuilders()
};
}
+ if (cmpMirage && cmpMirage.Foundation())
+ {
+ ret.foundation = {
+ "progress": cmpMirage.GetBuildPercentage()
+ };
+ }
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
{
ret.player = cmpOwnership.GetOwner();
}
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
ret.rallyPoint = {'position': cmpRallyPoint.GetPositions()[0]}; // undefined or {x,z} object
}
var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
}
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"possibleStances": cmpUnitAI.GetPossibleStances(),
};
// Add some information needed for ungarrisoning
if (cmpUnitAI.IsGarrisoned() && ret.player !== undefined)
ret.template = "p" + ret.player + "&" + ret.template;
}
var cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
{
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
}
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
{
ret.gate = {
"locked": cmpGate.IsLocked(),
};
}
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
{
ret.alertRaiser = {
"level": cmpAlertRaiser.GetLevel(),
"canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
"hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
};
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
return ret;
};
/**
* Get additionnal entity info, rarely used in the gui
*/
GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
{
var ret = {
"armour": null,
"attack": null,
"barterMarket": null,
"buildingAI": null,
"healer": null,
+ "mirage": null,
"obstruction": null,
"turretParent":null,
"promotion": null,
"resourceCarrying": null,
"resourceDropsite": null,
"resourceGatherRates": null,
"resourceSupply": null,
};
+ var cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
+ if (cmpMirage)
+ ret.mirage = true;
+
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? show all?
ret.attack = cmpAttack.GetAttackStrengths(type);
var range = cmpAttack.GetRange(type);
ret.attack.type = type;
ret.attack.minRange = range.min;
ret.attack.maxRange = range.max;
var timers = cmpAttack.GetTimers(type);
ret.attack.prepareTime = timers.prepare;
ret.attack.repeatTime = timers.repeat;
if (type == "Ranged")
{
ret.attack.elevationBonus = range.elevationBonus;
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the rage in front of it, no spread. So angle = 0
ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack.elevationAdaptedRange = ret.attack.maxRange;
}
}
else
{
// not a ranged attack, set some defaults
ret.attack.elevationBonus = 0;
ret.attack.elevationAdaptedRange = ret.attack.maxRange;
}
}
var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
{
ret.armour = cmpArmour.GetArmourStrengths();
}
var cmpAuras = Engine.QueryInterface(ent, IID_Auras)
if (cmpAuras)
{
ret.auras = cmpAuras.GetDescriptions();
}
var cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
{
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
}
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
{
ret.obstruction = {
"controlGroup": cmpObstruction.GetControlGroup(),
"controlGroup2": cmpObstruction.GetControlGroup2(),
};
}
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
{
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"gatherers": cmpResourceSupply.GetGatherers()
};
}
+ if (cmpMirage && cmpMirage.ResourceSupply())
+ {
+ ret.resourceSupply = {
+ "max": cmpMirage.GetMaxAmount(),
+ "amount": cmpMirage.GetAmount(),
+ "type": cmpMirage.GetType(),
+ "isInfinite": cmpMirage.IsInfinite()
+ };
+ }
var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
}
var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes()
};
}
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
{
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
}
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterMarket = { "prices": cmpBarter.GetPrices() };
}
var cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
{
ret.healer = {
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
}
return ret;
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var rot = {x:0, y:0, z:0};
var pos = {x:cmd.x,z:cmd.z};
pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z);
var elevationBonus = cmd.elevationBonus || 0;
var range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, extendedName)
{
var name = extendedName;
// Special case for garrisoned units which have a extended template
if (extendedName.indexOf("&") != -1)
name = extendedName.slice(extendedName.indexOf("&")+1);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(name);
if (!template)
return null;
var ret = {};
if (template.Armour)
{
ret.armour = {
"hack": ApplyValueModificationsToTemplate("Armour/Hack", +template.Armour.Hack, player, template),
"pierce": ApplyValueModificationsToTemplate("Armour/Pierce", +template.Armour.Pierce, player, template),
"crush": ApplyValueModificationsToTemplate("Armour/Crush", +template.Armour.Crush, player, template),
};
}
if (template.Attack)
{
ret.attack = {};
for (var type in template.Attack)
{
ret.attack[type] = {
"hack": ApplyValueModificationsToTemplate("Attack/"+type+"/Hack", +(template.Attack[type].Hack || 0), player, template),
"pierce": ApplyValueModificationsToTemplate("Attack/"+type+"/Pierce", +(template.Attack[type].Pierce || 0), player, template),
"crush": ApplyValueModificationsToTemplate("Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0), player, template),
"minRange": ApplyValueModificationsToTemplate("Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0), player, template),
"maxRange": ApplyValueModificationsToTemplate("Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange, player, template),
"elevationBonus": ApplyValueModificationsToTemplate("Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0), player, template),
};
}
}
if (template.Auras)
{
ret.auras = {};
for each (var aura in template.Auras)
if (aura.AuraName)
ret.auras[aura.AuraName] = aura.AuraDescription || null;
}
if (template.BuildRestrictions)
{
// required properties
ret.buildRestrictions = {
"placementType": template.BuildRestrictions.PlacementType,
"territory": template.BuildRestrictions.Territory,
"category": template.BuildRestrictions.Category,
};
// optional properties
if (template.BuildRestrictions.Distance)
{
ret.buildRestrictions.distance = {
"fromCategory": template.BuildRestrictions.Distance.FromCategory,
};
if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance;
if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance;
}
}
if (template.TrainingRestrictions)
{
ret.trainingRestrictions = {
"category": template.TrainingRestrictions.Category,
};
}
if (template.Cost)
{
ret.cost = {};
if (template.Cost.Resources.food) ret.cost.food = ApplyValueModificationsToTemplate("Cost/Resources/food", +template.Cost.Resources.food, player, template);
if (template.Cost.Resources.wood) ret.cost.wood = ApplyValueModificationsToTemplate("Cost/Resources/wood", +template.Cost.Resources.wood, player, template);
if (template.Cost.Resources.stone) ret.cost.stone = ApplyValueModificationsToTemplate("Cost/Resources/stone", +template.Cost.Resources.stone, player, template);
if (template.Cost.Resources.metal) ret.cost.metal = ApplyValueModificationsToTemplate("Cost/Resources/metal", +template.Cost.Resources.metal, player, template);
if (template.Cost.Population) ret.cost.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, player, template);
if (template.Cost.PopulationBonus) ret.cost.populationBonus = ApplyValueModificationsToTemplate("Cost/PopulationBonus", +template.Cost.PopulationBonus, player, template);
if (template.Cost.BuildTime) ret.cost.time = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, player, template);
}
if (template.Footprint)
{
ret.footprint = {"height": template.Footprint.Height};
if (template.Footprint.Square)
ret.footprint.square = {"width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"]};
else if (template.Footprint.Circle)
ret.footprint.circle = {"radius": +template.Footprint.Circle["@radius"]};
else
warn("[GetTemplateData] Unrecognized Footprint type");
}
if (template.Obstruction)
{
ret.obstruction = {
"active": ("" + template.Obstruction.Active == "true"),
"blockMovement": ("" + template.Obstruction.BlockMovement == "true"),
"blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"),
"blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"),
"blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"),
"disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"),
"disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"),
"shape": {}
};
if (template.Obstruction.Static)
{
ret.obstruction.shape.type = "static";
ret.obstruction.shape.width = +template.Obstruction.Static["@width"];
ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"];
}
else if (template.Obstruction.Unit)
{
ret.obstruction.shape.type = "unit";
ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"];
}
else
{
ret.obstruction.shape.type = "cluster";
}
}
if (template.Pack)
{
ret.pack = {
"state": template.Pack.State,
"time": ApplyValueModificationsToTemplate("Pack/Time", +template.Pack.Time, player, template),
};
}
if (template.Health)
{
ret.health = Math.round(ApplyValueModificationsToTemplate("Health/Max", +template.Health.Max, player, template));
}
if (template.Identity)
{
ret.selectionGroupName = template.Identity.SelectionGroupName;
ret.name = {
"specific": (template.Identity.SpecificName || template.Identity.GenericName),
"generic": template.Identity.GenericName
};
ret.icon = template.Identity.Icon;
ret.tooltip = template.Identity.Tooltip;
ret.gateConversionTooltip = template.Identity.GateConversionTooltip;
ret.requiredTechnology = template.Identity.RequiredTechnology;
ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity);
}
if (template.UnitMotion)
{
ret.speed = {
"walk": ApplyValueModificationsToTemplate("UnitMotion/WalkSpeed", +template.UnitMotion.WalkSpeed, player, template),
};
if (template.UnitMotion.Run) ret.speed.run = ApplyValueModificationsToTemplate("UnitMotion/Run/Speed", +template.UnitMotion.Run.Speed, player, template);
}
if (template.Trader)
ret.trader = template.Trader;
if (template.WallSet)
{
ret.wallSet = {
"templates": {
"tower": template.WallSet.Templates.Tower,
"gate": template.WallSet.Templates.Gate,
"long": template.WallSet.Templates.WallLong,
"medium": template.WallSet.Templates.WallMedium,
"short": template.WallSet.Templates.WallShort,
},
"maxTowerOverlap": +template.WallSet.MaxTowerOverlap,
"minTowerOverlap": +template.WallSet.MinTowerOverlap,
};
}
if (template.WallPiece)
{
ret.wallPiece = {"length": +template.WallPiece.Length};
}
return ret;
};
GuiInterface.prototype.GetTechnologyData = function(player, name)
{
var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager);
var template = cmpTechTempMan.GetTemplate(name);
if (!template)
{
warn("Tried to get data for invalid technology: " + name);
return null;
}
var ret = {};
// Get specific name for this civ or else the generic specific name
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var specific = undefined;
if (template.specificName)
{
if (template.specificName[cmpPlayer.GetCiv()])
specific = template.specificName[cmpPlayer.GetCiv()];
else
specific = template.specificName['generic'];
}
ret.name = {
"specific": specific,
"generic": template.genericName,
};
if (template.icon)
ret.icon = "technologies/" + template.icon;
else
ret.icon = null;
ret.cost = {
"food": template.cost ? (+template.cost.food) : 0,
"wood": template.cost ? (+template.cost.wood) : 0,
"metal": template.cost ? (+template.cost.metal) : 0,
"stone": template.cost ? (+template.cost.stone) : 0,
"time": template.researchTime ? (+template.researchTime) : 0,
}
ret.tooltip = template.tooltip;
if (template.requirementsTooltip)
ret.requirementsTooltip = template.requirementsTooltip;
else
ret.requirementsTooltip = "";
if (template.requirements.class)
ret.classRequirements = {"class": template.requirements.class, "number": template.requirements.number};
ret.description = template.description;
return ret;
};
GuiInterface.prototype.IsTechnologyResearched = function(player, tech)
{
if (!tech)
return true;
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, tech)
{
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(tech);
};
// Returns technologies that are being actively researched, along with
// which entity is researching them and how far along the research is.
GuiInterface.prototype.GetStartedResearch = function(player)
{
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
var ret = {};
for (var tech in cmpTechnologyManager.GetTechsStarted())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
var cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
else
ret[tech].progress = 0;
}
return ret;
};
// Returns the battle state of the player.
GuiInterface.prototype.GetBattleState = function(player)
{
var cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
return cmpBattleDetection.GetState();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
var cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
return cmpAttackDetection.GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, amounts)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
return cmpPlayer.GetNeededResources(amounts);
};
GuiInterface.prototype.AddTimeNotification = function(notification)
{
var time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
notification.endTime = notification.duration + time;
notification.id = ++this.timeNotificationID;
this.timeNotifications.push(notification);
this.timeNotifications.sort(function (n1, n2){return n2.endTime - n1.endTime});
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
for (var i in this.timeNotifications)
{
if (this.timeNotifications[i].id == notificationID)
{
this.timeNotifications.splice(i);
return;
}
}
};
GuiInterface.prototype.GetTimeNotifications = function(playerID)
{
var time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
var toDelete = [];
for (var n of this.timeNotifications)
{
n.time = n.endTime - time;
if (n.time < 0)
toDelete.push(n.id);
}
for (var id of toDelete)
this.DeleteTimeNotification(id);
return this.timeNotifications;
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
{
if (!notification.duration)
notification.duration = 10000;
this.AddTimeNotification(notification);
return;
}
this.notifications.push(notification);
};
GuiInterface.prototype.GetNextNotification = function()
{
if (this.notifications.length)
return this.notifications.pop();
else
return false;
};
GuiInterface.prototype.GetAvailableFormations = function(player, data)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player);
return cmpPlayer.GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
var r = {};
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return r;
r.name = template.Formation.FormationName;
r.tooltip = template.Formation.DisabledTooltip || "";
r.icon = template.Formation.Icon;
return r;
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
for each (var ent in data.ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
// GetLastFormationName is named in a strange way as it (also) is
// the value of the current formation (see Formation.js LoadFormation)
if (cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
return true;
}
}
return false;
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for each (var ent in data.ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmpUnitAI.GetStanceName() == data.stance)
return true;
}
}
return false;
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var playerColours = {}; // cache of owner -> colour map
for each (var ent in cmd.entities)
{
var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's colour:
var owner = -1;
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
var colour = playerColours[owner];
if (!colour)
{
colour = {"r":1, "g":1, "b":1};
var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
if (cmpPlayer)
colour = cmpPlayer.GetColour();
playerColours[owner] = colour;
}
cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha}, cmd.selected);
}
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
for each (var ent in cmd.entities)
{
var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.SetEnabled(cmd.enabled);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player);
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player);
// If there are some rally points already displayed, first hide them
for each (var ent in this.entsRallyPointsDisplayed)
{
var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities
for each (var ent in cmd.entities)
{
var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location)
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position
var pos;
if (cmd.x && cmd.z)
pos = cmd;
else
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({'x': pos.x, 'y': pos.z}); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for each (var posi in cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({'x': posi.x, 'y': posi.z});
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
var result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": [],
}
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
{
this.placementEntity = undefined;
}
else
{
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
}
if (this.placementEntity)
{
var ent = this.placementEntity[1];
// Move the preview into the right location
var pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
// Set it to a red shade if this is an invalid location
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColour(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
var wallSet = cmd.wallSet;
var start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
var end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// do some entity cache management and check for snapping
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// we're clearing the preview, clear the entity cache and bail
var numCleared = 0;
for (var tpl in this.placementWallEntities)
{
for each (var ent in this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// keep template data around
}
return false;
}
else
{
// Move all existing cached entities outside of the world and reset their use count
for (var tpl in this.placementWallEntities)
{
for each (var ent in this.placementWallEntities[tpl].entities)
{
var pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before
for each (var tpl in wallSet.templates)
{
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, tpl),
};
// ensure that the loaded template data contains a wallPiece component
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
}
// prevent division by zero errors further on if the start and end positions are the same
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
var snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
var startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
var endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// clear the single-building preview entity (we'll be rolling our own)
this.SetBuildingPlacementPreview(player, {"template": ""});
// --------------------------------------------------------------------------------
// calculate wall placement and position preview entities
var result = {
"pieces": [],
"cost": {"food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0},
};
var previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
var startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length > 0 && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
var startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
var cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
{
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)],
"excludeFromResult": true, // preview only, must not appear in the result
});
}
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
});
}
if (end.pos)
{
// Analogous to the starting side case above
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
var endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []);
previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
var endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
var cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
{
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)],
"excludeFromResult": true
});
}
}
}
else
{
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
});
}
}
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
var allPiecesValid = true;
var numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
for (var i = 0; i < previewEntities.length; ++i)
{
var entInfo = previewEntities[i];
var ent = null;
var tpl = entInfo.template;
var tplData = this.placementWallEntities[tpl].templateData;
var entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
{
// reuse an existing one
ent = entPool.entities[entPool.numUsed];
}
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// move piece to right location
// TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
if (tpl === wallSet.templates.tower)
{
var terrainGroundPrev = null;
var terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
var targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
var primaryControlGroup = ent;
var secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
// check whether this wall piece can be validly positioned here
var validPlacement = false;
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region
// tell GetLosVisibility to force RetainInFog because preview entities set this to false,
// which would show them as hidden instead of fogged
// TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
var visible = (cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
if (visible)
{
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement
validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success);
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
numRequiredPieces++;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
result.cost.food += tplData.cost.food;
result.cost.wood += tplData.cost.wood;
result.cost.stone += tplData.cost.stone;
result.cost.metal += tplData.cost.metal;
result.cost.population += tplData.cost.population;
result.cost.populationBonus += tplData.cost.populationBonus;
result.cost.time += tplData.cost.time;
}
var canAfford = true;
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
var canAfford = false;
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColour(1, 1, 1, 1);
}
entPool.numUsed++;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateMgr.GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
// (TODO: break unlikely ties by choosing the lowest entity ID)
var minDist2 = -1;
var minDistEntitySnapData = null;
var radius2 = data.snapRadius * data.snapRadius;
for each (var ent in data.snapEntities)
{
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
var pos = cmpPosition.GetPosition();
var dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {"x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (template.BuildRestrictions.Category == "Dock")
{
var angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {"x": data.x, "z": data.z, "angle": angle};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
// Ignore if no entity was passed
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerEntities = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) {
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return false;
var cmpIdentity = Engine.QueryInterface(e, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass(data.idleClass))
return false;
return true;
});
var idleUnits = [];
for (var j = 0; j < playerEntities.length; ++j)
{
var ent = playerEntities[j];
if (ent <= data.prevUnit|0 || data.excludeUnits.indexOf(ent) > -1)
continue;
idleUnits.push(ent);
playerEntities.splice(j--, 1);
if (data.limit && idleUnits.length >= data.limit)
break;
}
return idleUnits;
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
var cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
var firstMarket = cmpEntityTrader.GetFirstMarket();
var secondMarket = cmpEntityTrader.GetSecondMarket();
var result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGain();
}
else if (data.target === secondMarket)
{
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGain(),
};
}
else if (!firstMarket)
{
result = {"type": "set first"};
}
else if (!secondMarket)
{
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
}
else
{
// Else both markets are not null and target is different from them
result = {"type": "set first"};
}
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
if (!cmpAttack)
return false;
return cmpAttack.CanAttack(data.target);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
var cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
cmpPathfinder.SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
cmpObstructionManager.SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for each (var ent in data.entities)
{
var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var traders = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) {
return Engine.QueryInterface(e, IID_Trader);
});
var landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
var shipTrader = { "total": 0, "trading": 0 };
for each (var ent in traders)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
var holder = cmpUnitAI.order.data.target;
var cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player, tradingGoods)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
return cmpPlayer.GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
var exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetExtendedEntityState": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"GetTechnologyData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNextNotification": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"SetSelectionHighlight": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"DisplayRallyPoint": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
else
throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 15612)
@@ -0,0 +1,149 @@
+const VIS_HIDDEN = 0;
+const VIS_FOGGED = 1;
+const VIS_VISIBLE = 2;
+
+function Mirage() {}
+
+Mirage.prototype.Schema =
+ "Mirage entities replace real entities in the fog-of-war." +
+ "";
+
+Mirage.prototype.Init = function()
+{
+ this.player = null;
+ this.parent = INVALID_ENTITY;
+
+ this.foundation = false;
+ this.buildPercentage = null;
+
+ this.health = false;
+ this.maxHitpoints = null;
+ this.hitpoints = null;
+ this.needsRepair = null;
+
+ this.resourceSupply = false;
+ this.maxAmount = null;
+ this.amount = null;
+ this.type = null;
+ this.isInfinite = null;
+};
+
+Mirage.prototype.SetParent = function(ent)
+{
+ this.parent = ent;
+};
+
+Mirage.prototype.GetPlayer = function()
+{
+ return this.player;
+};
+
+Mirage.prototype.SetPlayer = function(player)
+{
+ this.player = player;
+};
+
+// ============================
+// Parent entity data
+
+// Foundation data
+
+Mirage.prototype.AddFoundation = function(buildPercentage)
+{
+ this.foundation = true;
+ this.buildPercentage = buildPercentage;
+};
+
+Mirage.prototype.Foundation = function()
+{
+ return this.foundation;
+};
+
+Mirage.prototype.GetBuildPercentage = function()
+{
+ return this.buildPercentage;
+};
+
+// Health data
+
+Mirage.prototype.AddHealth = function(maxHitpoints, hitpoints, needsRepair)
+{
+ this.health = true;
+ this.maxHitpoints = maxHitpoints;
+ this.hitpoints = Math.ceil(hitpoints);
+ this.needsRepair = needsRepair;
+};
+
+Mirage.prototype.Health = function()
+{
+ return this.health;
+};
+
+Mirage.prototype.GetMaxHitpoints = function()
+{
+ return this.maxHitpoints;
+};
+
+Mirage.prototype.GetHitpoints = function()
+{
+ return this.hitpoints;
+};
+
+Mirage.prototype.NeedsRepair = function()
+{
+ return this.needsRepair;
+};
+
+// ResourceSupply data
+
+Mirage.prototype.AddResourceSupply = function(maxAmount, amount, type, isInfinite)
+{
+ this.resourceSupply = true;
+ this.maxAmount = maxAmount;
+ this.amount = amount;
+ this.type = type;
+ this.isInfinite = isInfinite;
+};
+
+Mirage.prototype.ResourceSupply = function()
+{
+ return this.resourceSupply;
+};
+
+Mirage.prototype.GetMaxAmount = function()
+{
+ return this.maxAmount;
+};
+
+Mirage.prototype.GetAmount = function()
+{
+ return this.amount;
+};
+
+Mirage.prototype.GetType = function()
+{
+ return this.type;
+};
+
+Mirage.prototype.IsInfinite = function()
+{
+ return this.isInfinite;
+};
+
+// ============================
+
+Mirage.prototype.OnVisibilityChanged = function(msg)
+{
+ if (msg.player == this.player && msg.newVisibility == VIS_VISIBLE && this.parent == INVALID_ENTITY)
+ Engine.DestroyEntity(this.entity);
+};
+
+Mirage.prototype.OnDestroy = function(msg)
+{
+ if (this.parent == INVALID_ENTITY)
+ return;
+
+ Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: this.parent });
+};
+
+Engine.RegisterComponentType(IID_Mirage, "Mirage", Mirage);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 15612)
@@ -1,347 +1,356 @@
function ResourceGatherer() {}
ResourceGatherer.prototype.Schema =
"Lets the unit gather resources from entities that have the ResourceSupply component." +
"" +
"2.0" +
"1.0" +
"" +
"1" +
"3" +
"3" +
"2" +
"" +
"" +
"10" +
"10" +
"10" +
"10" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
ResourceGatherer.prototype.Init = function()
{
this.carrying = {}; // { generic type: integer amount currently carried }
// (Note that this component supports carrying multiple types of resources,
// each with an independent capacity, but the rest of the game currently
// ensures and assumes we'll only be carrying one type at once)
// The last exact type gathered, so we can render appropriate props
this.lastCarriedType = undefined; // { generic, specific }
};
/**
* Returns data about what resources the unit is currently carrying,
* in the form [ {"type":"wood", "amount":7, "max":10} ]
*/
ResourceGatherer.prototype.GetCarryingStatus = function()
{
var ret = [];
for (var type in this.carrying)
{
ret.push({
"type": type,
"amount": this.carrying[type],
"max": +this.GetCapacities()[type]
});
}
return ret;
};
/**
* Used to instantly give resources to unit
* @param resources The same structure as returned form GetCarryingStatus
*/
ResourceGatherer.prototype.GiveResources = function(resources)
{
for each (var resource in resources)
{
this.carrying[resource.type] = +(resource.amount);
}
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
};
/**
* Returns the generic type of one particular resource this unit is
* currently carrying, or undefined if none.
*/
ResourceGatherer.prototype.GetMainCarryingType = function()
{
// Return the first key, if any
for (var type in this.carrying)
return type;
return undefined;
};
/**
* Returns the exact resource type we last picked up, as long as
* we're still carrying something similar enough, in the form
* { generic, specific }
*/
ResourceGatherer.prototype.GetLastCarriedType = function()
{
if (this.lastCarriedType && this.lastCarriedType.generic in this.carrying)
return this.lastCarriedType;
else
return undefined;
};
ResourceGatherer.prototype.GetGatherRates = function()
{
var ret = {};
var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
var baseSpeed = ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity) * cmpPlayer.GetGatherRateMultiplier();
for (var r in this.template.Rates)
{
var rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity);
ret[r] = rate * baseSpeed;
}
return ret;
};
ResourceGatherer.prototype.GetCapacities = function()
{
var ret = {};
for (var r in this.template.Capacities)
{
ret[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], this.entity);
}
return ret;
};
ResourceGatherer.prototype.GetRange = function()
{
return { "max": +this.template.MaxDistance, "min": 0 };
// maybe this should depend on the unit or target or something?
};
/**
* Try to gather treasure
* @return 'true' if treasure is successfully gathered and 'false' in the other case
*/
ResourceGatherer.prototype.TryInstantGather = function(target)
{
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
if (type.generic != "treasure") return false;
var status = cmpResourceSupply.TakeResources(cmpResourceSupply.GetCurrentAmount());
var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
cmpPlayer.AddResource(type.specific, status.amount);
var cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseTreasuresCollectedCounter();
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (cmpTrigger)
cmpTrigger.CallEvent("TreasureCollected", {"player": cmpPlayer.GetPlayerID(), "type": type.specific, "amount": status.amount });
return true;
};
/**
* Gather from the target entity. This should only be called after a successful range check,
* and if the target has a compatible ResourceSupply.
* Call interval will be determined by gather rate, so always gather 1 amount when called.
*/
ResourceGatherer.prototype.PerformGather = function(target)
{
if (!this.GetTargetGatherRate(target))
return { "exhausted": true };
var gatherAmount = 1;
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
// Initialise the carried count if necessary
if (!this.carrying[type.generic])
this.carrying[type.generic] = 0;
// Find the maximum so we won't exceed our capacity
var maxGathered = this.GetCapacities()[type.generic] - this.carrying[type.generic];
var status = cmpResourceSupply.TakeResources(Math.min(gatherAmount, maxGathered));
this.carrying[type.generic] += status.amount;
this.lastCarriedType = type;
// Update stats of how much the player collected.
// (We have to do it here rather than at the dropsite, because we
// need to know what subtype it was)
var cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific);
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
// Tell the target we're gathering from it
Engine.PostMessage(target, MT_ResourceGather,
{ "entity": target, "gatherer": this.entity });
return {
"amount": status.amount,
"exhausted": status.exhausted,
"filled": (this.carrying[type.generic] >= this.GetCapacities()[type.generic])
};
};
/**
* Compute the amount of resources collected per second from the target.
* Returns 0 if resources cannot be collected (e.g. the target doesn't
* exist, or is the wrong type).
*/
ResourceGatherer.prototype.GetTargetGatherRate = function(target)
{
var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+ var type;
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
- if (!cmpResourceSupply)
+ var cmpMirage = Engine.QueryInterface(target, IID_Mirage);
+ if (cmpResourceSupply)
+ type = cmpResourceSupply.GetType();
+ else if (cmpMirage && cmpMirage.ResourceSupply())
+ type = cmpMirage.GetType();
+ else
return 0;
- var type = cmpResourceSupply.GetType();
-
var rates = this.GetGatherRates();
var rate;
if (type.specific && rates[type.generic+"."+type.specific])
{
rate = rates[type.generic+"."+type.specific] / cmpPlayer.GetCheatTimeMultiplier();
}
else if (type.generic && rates[type.generic])
{
rate = rates[type.generic] / cmpPlayer.GetCheatTimeMultiplier();
}
+ if (cmpMirage)
+ return rate || 0;
+
// Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect. (GetDiminishingReturns will return null.)
+ // We can assume that for resources that are miraged this is the case. (else just add the diminishing returns data to the mirage data and remove the
+ // early return above)
// Note to people looking to change in a template: This is a bit complicated. Basically, the lower that number is
// the steeper diminishing returns will be. I suggest playing around with Wolfram Alpha or a graphing calculator a bit.
// In each of the following links, replace 0.65 with the gather rate of your worker for the resource with diminishing returns and
// 14 with the constant you wish to use to control the diminishing returns.
// (In this case 0.65 is the women farming rate, in resources/second, and 14 is a good constant for farming.)
// This is the gather rate in resources/second of each individual worker as the total number of workers goes up:
// http://www.wolframalpha.com/input/?i=plot+%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+*+0.65+from+1+to+5
// This is the total output of the resource in resources/second:
// http://www.wolframalpha.com/input/?i=plot+x%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+*+0.65+from+1+to+5
// This is the fraction of a worker each new worker is worth (the 5th worker in this example is only producing about half as much as the first one):
// http://www.wolframalpha.com/input/?i=plot+x%281%2F2+cos%28%28x-1%29*pi%2F14%29+%2B+1%2F2%29+-++%28x-1%29%281%2F2+cos%28%28x-2%29*pi%2F14%29+%2B+1%2F2%29+from+x%3D1+to+5+and+y%3D0+to+1
// Here's how this technically works:
// The cosine function is an oscillating curve, normally between -1 and 1. Multiplying by 0.5 squishes that down to
// between -0.5 and 0.5. Adding 0.5 to that changes the range to 0 to 1. The diminishingReturns constant
// adjusts the period of the curve.
// Alternatively, just find scythetwirler (who came up with the math here) or alpha123 (who wrote the code) on IRC.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var diminishingReturns = cmpResourceSupply.GetDiminishingReturns();
if (diminishingReturns)
rate = (0.5 * Math.cos((cmpResourceSupply.GetGatherers().length - 1) * Math.PI / diminishingReturns) + 0.5) * rate;
return rate || 0;
};
/**
* Returns whether this unit can carry more of the given type of resource.
* (This ignores whether the unit is actually able to gather that
* resource type or not.)
*/
ResourceGatherer.prototype.CanCarryMore = function(type)
{
var amount = (this.carrying[type] || 0);
return (amount < this.GetCapacities()[type]);
};
/**
* Returns whether this unit is carrying any resources of a type that is
* not the requested type. (This is to support cases where the unit is
* only meant to be able to carry one type at once.)
*/
ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType)
{
for (var type in this.carrying)
if (type != exceptedType)
return true;
return false;
};
/**
* Transfer our carried resources to our owner immediately.
* Only resources of the given types will be transferred.
* (This should typically be called after reaching a dropsite).
*/
ResourceGatherer.prototype.CommitResources = function(types)
{
var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
for each (var type in types)
{
if (type in this.carrying)
{
cmpPlayer.AddResource(type, this.carrying[type]);
delete this.carrying[type];
}
}
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
};
/**
* Drop all currently-carried resources.
* (Currently they just vanish after being dropped - we don't bother depositing
* them onto the ground.)
*/
ResourceGatherer.prototype.DropResources = function()
{
this.carrying = {};
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
};
Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 15612)
@@ -1,5814 +1,5833 @@
function UnitAI() {}
UnitAI.prototype.Schema =
"Controls the unit's movement, attacks, etc, in response to commands from the player." +
"" +
"" +
"" +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"standground" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"skittish" +
"domestic" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"" +
"";
// Unit stances.
// There some targeting options:
// targetVisibleEnemies: anything in vision range is a viable target
// targetAttackersAlways: anything that hurts us is a viable target,
// possibly overriding user orders!
// targetAttackersPassive: anything that hurts us is a viable target,
// if we're on a passive/unforced order (e.g. gathering/building)
// There are some response options, triggered when targets are detected:
// respondFlee: run away
// respondChase: start chasing after the enemy
// respondChaseBeyondVision: start chasing, and don't stop even if it's out
// of this unit's vision range (though still visible to the player)
// respondStandGround: attack enemy but don't move at all
// respondHoldGround: attack enemy but don't move far from current position
// TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts,
// do worry around armies slaughtering the guy standing next to you), etc.
var g_Stances = {
"violent": {
targetVisibleEnemies: true,
targetAttackersAlways: true,
targetAttackersPassive: true,
respondFlee: false,
respondChase: true,
respondChaseBeyondVision: true,
respondStandGround: false,
respondHoldGround: false,
},
"aggressive": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: true,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: false,
},
"defensive": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: true,
},
"passive": {
targetVisibleEnemies: false,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: true,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: false,
},
"standground": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: true,
respondHoldGround: false,
},
};
// See ../helpers/FSM.js for some documentation of this FSM specification syntax
UnitAI.prototype.UnitFsmSpec = {
// Default event handlers:
"MoveCompleted": function() {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"MoveStarted": function() {
// ignore spurious movement messages
},
"ConstructionFinished": function(msg) {
// ignore uninteresting construction messages
},
"LosRangeUpdate": function(msg) {
// ignore newly-seen units by default
},
"LosHealRangeUpdate": function(msg) {
// ignore newly-seen injured units by default
},
"Attacked": function(msg) {
// ignore attacker
},
"HealthChanged": function(msg) {
// ignore
},
"PackFinished": function(msg) {
// ignore
},
"PickupCanceled": function(msg) {
// ignore
},
"GuardedAttacked": function(msg) {
// ignore
},
// Formation handlers:
"FormationLeave": function(msg) {
// ignore when we're not in FORMATIONMEMBER
},
// Called when being told to walk as part of a formation
"Order.FormationWalk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z);
this.SetNextStateAlwaysEntering("FORMATIONMEMBER.WALKING");
},
// Special orders:
// (these will be overridden by various states)
"Order.LeaveFoundation": function(msg) {
// If foundation is not ally of entity, or if entity is unpacked siege,
// ignore the order
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) || this.IsPacking() || this.CanPack() || this.IsTurret())
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
// Individual orders:
// (these will switch the unit out of formation mode)
"Order.Stop": function(msg) {
// We have no control over non-domestic animals.
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
// Stop moving immediately.
this.StopMoving();
this.FinishOrder();
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
"Order.Walk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
if (!this.order.data.max)
this.MoveToPoint(this.order.data.x, this.order.data.z);
else
this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
},
"Order.WalkAndFight": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
this.MoveToPoint(this.order.data.x, this.order.data.z);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING"); // WalkAndFight not applicable for animals
else
this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING");
},
"Order.WalkToTarget": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
var ok = this.MoveToTarget(this.order.data.target);
if (ok)
{
// We've started walking to the given point
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.FinishOrder();
}
},
"Order.PickupUnit": function(msg) {
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return;
}
// Check if we need to move TODO implement a better way to know if we are on the shoreline
var needToMove = true;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (this.lastShorelinePosition && cmpPosition && (this.lastShorelinePosition.x == cmpPosition.GetPosition().x)
&& (this.lastShorelinePosition.z == cmpPosition.GetPosition().z))
{
// we were already on the shoreline, and have not moved since
if (DistanceBetweenEntities(this.entity, this.order.data.target) < 50)
needToMove = false;
}
// TODO: what if the units are on a cliff ? the ship will go below the cliff
// and the units won't be able to garrison. Should go to the nearest (accessible) shore
if (needToMove && this.MoveToTarget(this.order.data.target))
{
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
}
},
"Order.Guard": function(msg) {
if (!this.AddGuard(this.order.data.target))
{
this.FinishOrder();
return;
}
if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
else
this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
},
"Order.Flee": function(msg) {
// We use the distance between the entities to account for ranged attacks
var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1))
{
// We've started fleeing from the given target
if (this.IsAnimal())
this.SetNextState("ANIMAL.FLEEING");
else
this.SetNextState("INDIVIDUAL.FLEEING");
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.FinishOrder();
}
},
"Order.Attack": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Work out how to attack the given target
var type = this.GetBestAttackAgainst(this.order.data.target);
if (!type)
{
// Oops, we can't attack at all
this.FinishOrder();
return;
}
this.order.data.attackType = type;
// If we are already at the target, try attacking it from here
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.StopMoving();
// For packable units within attack range:
// 1. If unpacked, we can attack the target.
// 2. If packed, we first need to unpack, then follow case 1.
if (this.CanUnpack())
{
// Ignore unforced attacks
// TODO: use special stances instead?
if (!this.order.data.force)
{
this.FinishOrder();
return;
}
// Case 2: unpack
this.PushOrderFront("Unpack", { "force": true });
return;
}
if (this.order.data.attackType == this.oldAttackType)
{
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
else
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
}
else
{
if (this.IsAnimal())
this.SetNextStateAlwaysEntering("ANIMAL.COMBAT.ATTACKING");
else
this.SetNextStateAlwaysEntering("INDIVIDUAL.COMBAT.ATTACKING");
}
return;
}
// For packable units out of attack range:
// 1. If packed, we need to move to attack range and then unpack.
// 2. If unpacked, we first need to pack, then follow case 1.
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack)
{
// Ignore unforced attacks
// TODO: use special stances instead?
if (!this.order.data.force)
{
this.FinishOrder();
return;
}
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
}
// If we can't reach the target, but are standing ground, then abandon this attack order.
// Unless we're hunting, that's a special case where we should continue attacking our target.
if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || this.IsTurret())
{
this.FinishOrder();
return;
}
// Try to move within attack range
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// We've started walking to the given point
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
else
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
return;
}
// We can't reach the target, and can't move towards it,
// so abandon this attack order
this.FinishOrder();
},
"Order.Heal": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Healers can't heal themselves.
if (this.order.data.target == this.entity)
{
this.FinishOrder();
return;
}
// Check if the target is in range
if (this.CheckTargetRange(this.order.data.target, IID_Heal))
{
this.StopMoving();
this.SetNextState("INDIVIDUAL.HEAL.HEALING");
return;
}
// If we can't reach the target, but are standing ground,
// then abandon this heal order
if (this.GetStance().respondStandGround && !this.order.data.force)
{
this.FinishOrder();
return;
}
// Try to move within heal range
if (this.MoveToTargetRange(this.order.data.target, IID_Heal))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
return;
}
// We can't reach the target, and can't move towards it,
// so abandon this heal order
this.FinishOrder();
},
"Order.Gather": function(msg) {
// If the target is still alive, we need to kill it first
if (this.MustKillGatherTarget(this.order.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
if (!this.GetBestAttackAgainst(this.order.data.target))
{
// Oops, we can't attack at all - give up
// TODO: should do something so the player knows why this failed
this.FinishOrder();
return;
}
// The target was visible when this order was issued,
// but could now be invisible again.
if (!this.CheckTargetVisible(this.order.data.target))
{
if (this.order.data.secondTry === undefined)
{
this.order.data.secondTry = true;
this.PushOrderFront("Walk", this.order.data.lastPos);
}
else
{
// We couldn't move there, or the target moved away
this.FinishOrder();
}
return;
}
this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true });
return;
}
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try gathering it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING");
}
},
"Order.GatherNearPosition": function(msg) {
// Move the unit to the position to gather from.
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("INDIVIDUAL.GATHER.WALKING");
},
"Order.ReturnResource": function(msg) {
// Check if the dropsite is already in range
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes);
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
// Try to move to the dropsite
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
{
// We've started walking to the target
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
return;
}
// Oops, we can't reach the dropsite.
// Maybe we should try to pick another dropsite, to find an
// accessible one?
// For now, just give up.
this.StopMoving();
this.FinishOrder();
return;
},
"Order.Trade": function(msg) {
// We must check if this trader has both markets in case it was a back-to-work order
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader || ! cmpTrader.HasBothMarkets())
{
this.FinishOrder();
return;
}
var nextMarket = cmpTrader.GetNextMarket();
if (nextMarket == this.order.data.firstMarket)
var state = "TRADE.APPROACHINGFIRSTMARKET";
else
var state = "TRADE.APPROACHINGSECONDMARKET";
// TODO find the nearest way-point from our position, and start with it
this.waypoints = undefined;
if (this.MoveToMarket(nextMarket))
{
// We've started walking to the next market
this.SetNextState(state);
}
else
this.FinishOrder();
},
"Order.Repair": function(msg) {
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_Builder))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try repairing it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
this.SetNextStateAlwaysEntering("INDIVIDUAL.REPAIR.REPAIRING");
}
},
"Order.Garrison": function(msg) {
if (this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move to the garrison target.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
if (this.MoveToGarrisonRange(this.order.data.target))
{
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
}
else
{
// We do a range check before actually garrisoning
this.StopMoving();
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
}
},
"Order.Autogarrison": function(msg) {
this.SetNextState("INDIVIDUAL.AUTOGARRISON");
},
"Order.Alert": function(msg) {
this.alertRaiser = this.order.data.raiser;
// Find a target to garrison into, if we don't already have one
if (!this.alertGarrisoningTarget)
this.alertGarrisoningTarget = this.FindNearbyGarrisonHolder();
if (this.alertGarrisoningTarget)
this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget});
else
this.FinishOrder();
},
"Order.Cheering": function(msg) {
this.SetNextState("INDIVIDUAL.CHEERING");
},
"Order.Pack": function(msg) {
if (this.CanPack())
{
this.StopMoving();
this.SetNextState("INDIVIDUAL.PACKING");
}
},
"Order.Unpack": function(msg) {
if (this.CanUnpack())
{
this.StopMoving();
this.SetNextState("INDIVIDUAL.UNPACKING");
}
},
"Order.CancelPack": function(msg) {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
cmpPack.CancelPack();
this.FinishOrder();
},
"Order.CancelUnpack": function(msg) {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
cmpPack.CancelPack();
this.FinishOrder();
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
"Order.Walk": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("WALKING");
},
"Order.WalkAndFight": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("WALKINGANDFIGHTING");
},
"Order.MoveIntoFormation": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("FORMING");
},
// Only used by other orders to walk there in formation
"Order.WalkToTargetRange": function(msg) {
if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.WalkToTarget": function(msg) {
if (this.MoveToTarget(this.order.data.target))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.WalkToPointRange": function(msg) {
if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.Guard": function(msg) {
this.CallMemberFunction("Guard", [msg.data.target, false]);
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.Disband();
},
"Order.Stop": function(msg) {
this.CallMemberFunction("Stop", [false]);
this.FinishOrder();
},
"Order.Attack": function(msg) {
var target = msg.data.target;
var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
if (this.MoveToTargetAttackRange(target, target))
{
this.SetNextState("COMBAT.APPROACHING");
return;
}
}
this.FinishOrder();
return;
}
this.CallMemberFunction("Attack", [target, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
"Order.Garrison": function(msg) {
if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder))
{
this.FinishOrder();
return;
}
// Check if we are already in range, otherwise walk there
if (!this.CheckGarrisonRange(msg.data.target))
{
if (!this.CheckTargetVisible(msg.data.target))
{
this.FinishOrder();
return;
}
else
{
// Out of range; move there in formation
if (this.MoveToGarrisonRange(msg.data.target))
{
this.SetNextState("GARRISON.APPROACHING");
return;
}
}
}
this.SetNextState("GARRISON.GARRISONING");
},
"Order.Gather": function(msg) {
if (this.MustKillGatherTarget(msg.data.target))
{
// The target was visible when this order was given,
// but could now be invisible.
if (!this.CheckTargetVisible(msg.data.target))
{
if (msg.data.secondTry === undefined)
{
msg.data.secondTry = true;
this.PushOrderFront("Walk", msg.data.lastPos);
}
else
{
// We couldn't move there, or the target moved away
this.FinishOrder();
}
return;
}
this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true });
return;
}
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target isn't gatherable or not visible any more.
this.FinishOrder();
// TODO: Should we issue a gather-near-position order
// if the target isn't gatherable/doesn't exist anymore?
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Gather", [msg.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.GatherNearPosition": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
{
// Out of range; move there in formation
this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
return;
}
this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Heal": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Heal", [msg.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Repair": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The building was finished or destroyed
this.FinishOrder();
else
// Out of range move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.ReturnResource": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("ReturnResource", [msg.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Pack": function(msg) {
this.CallMemberFunction("Pack", [false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Unpack": function(msg) {
this.CallMemberFunction("Unpack", [false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"IDLE": {
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
},
},
"WALKING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
this.CallMemberFunction("ResetFinishOrder", []);
},
},
"WALKINGANDFIGHTING": {
"enter": function(msg) {
this.StartTimer(0, 1000);
},
"Timer": function(msg) {
// check if there are no enemies to attack
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopTimer();
},
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
this.CallMemberFunction("ResetFinishOrder", []);
},
},
"GARRISON":{
"enter": function() {
// If the garrisonholder should pickup, warn it so it can take needed action
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
},
"leave": function() {
// If a pickup has been requested and not yet canceled, cancel it
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"APPROACHING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
this.SetNextState("GARRISONING");
},
},
"GARRISONING": {
"enter": function() {
// If a pickup has been requested, cancel it as it will be requested by members
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
this.CallMemberFunction("Garrison", [this.order.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
},
},
"FORMING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, false);
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
{
this.CallMemberFunction("ResetFinishOrder", []);
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.FindInPosition();
}
},
"COMBAT": {
"APPROACHING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.CallMemberFunction("Attack", [this.order.data.target, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
},
"ATTACKING": {
// Wait for individual members to finish
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(false, false);
this.StartTimer(200, 200);
var target = this.order.data.target;
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(target);
this.PushOrderFront("WalkToTargetRange", { "target": target, "min": range.min, "max": range.max });
return;
}
this.FinishOrder();
return;
}
},
"Timer": function(msg) {
var target = this.order.data.target;
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(target);
this.FinishOrder();
this.PushOrderFront("Attack", { "target": target, "force": false });
return;
}
this.FinishOrder();
return;
}
},
"leave": function(msg) {
this.StopTimer();
},
},
},
"MEMBER": {
// Wait for individual members to finish
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
this.StartTimer(1000, 1000);
},
"Timer": function(msg) {
// Have all members finished the task?
if (!this.TestAllMemberFunction("HasFinishedOrder", []))
return;
this.CallMemberFunction("ResetFinishOrder", []);
// Execute the next order
if (this.FinishOrder())
{
// if WalkAndFight order, look for new target before moving again
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
},
"leave": function(msg) {
this.StopTimer();
},
},
},
// States for entities moving as part of a formation:
"FORMATIONMEMBER": {
"FormationLeave": function(msg) {
// We're not in a formation anymore, so no need to track this.
this.finishedOrder = false;
// Stop moving as soon as the formation disbands
this.StopMoving();
// If the controller handled an order but some members rejected it,
// they will have no orders and be in the FORMATIONMEMBER.IDLE state.
if (this.orderQueue.length)
{
// We're leaving the formation, so stop our FormationWalk order
if (this.FinishOrder())
return;
}
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
// Override the LeaveFoundation order since we're not doing
// anything more important (and we might be stuck in the WALKING
// state forever and need to get out of foundations in that case)
"Order.LeaveFoundation": function(msg) {
// If foundation is not ally of entity, or if entity is unpacked siege,
// ignore the order
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) || this.IsPacking() || this.CanPack() || this.IsTurret())
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("WALKINGTOPOINT");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
"IDLE": {
"enter": function() {
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
return true;
},
},
"WALKING": {
"enter": function () {
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpFormation && cmpVisual)
{
cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk"));
cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run"));
}
this.SelectAnimation("move");
},
// Occurs when the unit has reached its destination and the controller
// is done moving. The controller is notified.
"MoveCompleted": function(msg) {
// We can only finish this order if the move was really completed.
if (!msg.data.error && this.FinishOrder())
return;
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
{
cmpVisual.ResetMoveAnimation("walk");
cmpVisual.ResetMoveAnimation("run");
}
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.SetInPosition(this.entity);
},
},
// Special case used by Order.LeaveFoundation
"WALKINGTOPOINT": {
"enter": function() {
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.UnsetInPosition(this.entity);
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.FinishOrder();
},
},
},
// States for entities not part of a formation:
"INDIVIDUAL": {
"enter": function() {
// Sanity-checking
if (this.IsAnimal())
error("Animal got moved into INDIVIDUAL.* state");
},
"Attacked": function(msg) {
// Respond to attack if we always target attackers, or if we target attackers
// during passive orders (e.g. gathering/repairing are never forced)
if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (!this.order || !this.order.data || !this.order.data.force)))
{
this.RespondToTargetedEntities([msg.data.attacker]);
}
},
"GuardedAttacked": function(msg) {
// do nothing if we have a forced order in queue before the guard order
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type == "Guard")
break;
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
return;
}
// if we already are targeting another unit still alive, finish with it first
if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack"))
if (this.order.data.target != msg.data.attacker && this.TargetIsAlive(msg.data.attacker))
return;
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpIdentity && cmpIdentity.HasClass("Support") &&
cmpHealth && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
// if the attacker is a building and we can repair the guarded, repair it rather than attacking
var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI);
if (cmpBuildingAI && this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
{
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
// target the unit
if (this.CheckTargetVisible(msg.data.attacker))
this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false });
else
{
var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false });
// if we already had a WalkAndFight, keep only the most recent one in case the target has moved
if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight")
this.orderQueue.splice(1, 1);
}
},
"IDLE": {
"enter": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with an incorrect animation
var animationName = "idle";
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
animationName = cmpFormation.GetFormationAnimation(this.entity, animationName);
}
this.SelectAnimation(animationName);
// If the unit is guarding/escorting, go back to its duty
if (this.isGuardOf)
{
this.Guard(this.isGuardOf, false);
return true;
}
// The GUI and AI want to know when a unit is idle, but we don't
// want to send frequent spurious messages if the unit's only
// idle for an instant and will quickly go off and do something else.
// So we'll set a timer here and only report the idle event if we
// remain idle
this.StartTimer(1000);
// If a unit can heal and attack we first want to heal wounded units,
// so check if we are a healer and find whether there's anybody nearby to heal.
// (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
// If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
if (this.IsHealer() && this.FindNewHealTargets())
return true; // (abort the FSM transition since we may have already switched state)
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
if (this.FindNewTargets())
return true; // (abort the FSM transition since we may have already switched state)
// Nobody to attack - stay in idle
return false;
},
"leave": function() {
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
rangeMan.DisableActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
rangeMan.DisableActiveQuery(this.losHealRangeQuery);
this.StopTimer();
if (this.isIdle)
{
this.isIdle = false;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
"LosRangeUpdate": function(msg) {
if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
this.AttackEntitiesByPreference(msg.data.added);
}
},
"LosHealRangeUpdate": function(msg) {
this.RespondToHealableEntities(msg.data.added);
},
"Timer": function(msg) {
if (!this.isIdle)
{
this.isIdle = true;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
},
"WALKING": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.FinishOrder();
},
},
"WALKINGANDFIGHTING": {
"enter": function () {
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.StartTimer(0, 1000);
this.SelectAnimation("move");
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopTimer();
},
"MoveCompleted": function() {
this.FinishOrder();
},
},
"GUARD": {
"RemoveGuard": function() {
this.StopMoving();
this.FinishOrder();
},
"ESCORTING": {
"enter": function () {
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.StartTimer(0, 1000);
this.SelectAnimation("move");
this.SetHeldPositionOnEntity(this.isGuardOf);
return false;
},
"Timer": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.isGuardOf))
{
this.StopMoving();
this.FinishOrder();
return;
}
this.SetHeldPositionOnEntity(this.isGuardOf);
},
"leave": function(msg) {
this.SetMoveSpeed(this.GetWalkSpeed());
this.StopTimer();
},
"MoveStarted": function(msg) {
// Adapt the speed to the one of the target if needed
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange))
{
var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI);
if (cmpUnitAI)
{
var speed = cmpUnitAI.GetWalkSpeed();
if (speed < this.GetWalkSpeed())
this.SetMoveSpeed(speed);
}
}
},
"MoveCompleted": function() {
this.SetMoveSpeed(this.GetWalkSpeed());
if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("GUARDING");
},
},
"GUARDING": {
"enter": function () {
this.StartTimer(1000, 1000);
this.SetHeldPositionOnEntity(this.entity);
this.SelectAnimation("idle");
return false;
},
"LosRangeUpdate": function(msg) {
// Start attacking one of the newly-seen enemy (if any)
if (this.GetStance().targetVisibleEnemies)
this.AttackEntitiesByPreference(msg.data.added);
},
"Timer": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.isGuardOf))
{
this.FinishOrder();
return;
}
// then check is the target has moved
if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("ESCORTING");
else
{
// if nothing better to do, check if the guarded needs to be healed or repaired
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpHealth && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()))
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
}
}
},
"leave": function(msg) {
this.StopTimer();
},
},
},
"FLEEING": {
"enter": function() {
this.PlaySound("panic");
// Run quickly
var speed = this.GetRunSpeed();
this.SelectAnimation("move");
this.SetMoveSpeed(speed);
},
"HealthChanged": function() {
var speed = this.GetRunSpeed();
this.SetMoveSpeed(speed);
},
"leave": function() {
// Reset normal speed
this.SetMoveSpeed(this.GetWalkSpeed());
},
"MoveCompleted": function() {
// When we've run far enough, stop fleeing
this.FinishOrder();
},
// TODO: what if we run into more enemies while fleeing?
},
"COMBAT": {
"Order.LeaveFoundation": function(msg) {
// Ignore the order as we're busy.
return { "discardOrder": true };
},
"Attacked": function(msg) {
// If we're already in combat mode, ignore anyone else
// who's attacking us
},
"APPROACHING": {
"enter": function () {
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
// Show carried resources when walking.
this.SetGathererAnimationOverride();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// If the unit needs to unpack, do so
if (this.CanUnpack())
this.SetNextState("UNPACKING");
else
this.SetNextState("ATTACKING");
}
else
{
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.SetNextState("APPROACHING");
}
else
{
// Give up
this.FinishOrder();
}
}
},
"Attacked": function(msg) {
// If we're attacked by a close enemy, we should try to defend ourself
// but only if we're not forced to target something else
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force)))
{
this.RespondToTargetedEntities([msg.data.attacker]);
}
},
},
"UNPACKING": {
"enter": function() {
// If we're not in range yet (maybe we stopped moving), move to target again
if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
this.SetNextState("APPROACHING");
else
{
// Give up
this.FinishOrder();
}
return true;
}
// In range, unpack
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Unpack();
return false;
},
"PackFinished": function(msg) {
this.SetNextState("ATTACKING");
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore further attacks while unpacking
},
},
"ATTACKING": {
"enter": function() {
var target = this.order.data.target;
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
// if the target is a formation, save the attacking formation, and pick a member
if (cmpFormation)
{
this.order.data.formationTarget = target;
target = cmpFormation.GetClosestMember(this.entity);
this.order.data.target = target;
}
// Check the target is still alive and attackable
if
(
this.TargetIsAlive(target) &&
this.CanAttack(target, this.order.data.forceResponse || null) &&
!this.CheckTargetAttackRange(target, this.order.data.attackType)
)
{
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetAttackRange(target, this.order.data.attackType))
{
this.SetNextState("COMBAT.CHASING");
return;
}
}
}
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType);
// If the repeat time since the last attack hasn't elapsed,
// delay this attack to avoid attacking too fast.
var prepare = this.attackTimers.prepare;
if (this.lastAttacked)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
// add prefix + no capital first letter for attackType
var animationName = "attack_" + this.order.data.attackType.toLowerCase();
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
animationName = cmpFormation.GetFormationAnimation(this.entity, animationName);
}
this.SelectAnimation(animationName, false, 1.0, "attack");
this.SetAnimationSync(prepare, this.attackTimers.repeat);
this.StartTimer(prepare, this.attackTimers.repeat);
// TODO: we should probably only bother syncing projectile attacks, not melee
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false;
this.FaceTowardsTarget(this.order.data.target);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
// if the target is a formation, save the attacking formation, and pick a member
if (cmpFormation)
{
var thisObject = this;
var filter = function(t) {
return thisObject.TargetIsAlive(t) && thisObject.CanAttack(t, thisObject.order.data.forceResponse || null);
};
this.order.data.formationTarget = target;
target = cmpFormation.GetClosestMember(this.entity, filter);
this.order.data.target = target;
}
// Check the target is still alive and attackable
if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null))
{
// If we are hunting, first update the target position of the gather order so we know where will be the killed animal
if (this.order.data.hunting && this.orderQueue[1] && this.orderQueue[1].data.lastPos)
{
var cmpPosition = Engine.QueryInterface(this.order.data.target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
// Store the initial position, so that we can find the rest of the herd later
if (!this.orderQueue[1].data.initPos)
this.orderQueue[1].data.initPos = this.orderQueue[1].data.lastPos;
this.orderQueue[1].data.lastPos = cmpPosition.GetPosition();
// We still know where the animal is, so we shouldn't give up before going there
this.orderQueue[1].data.secondTry = undefined;
}
}
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
cmpAttack.PerformAttack(this.order.data.attackType, target);
// Check we can still reach the target for the next attack
if (this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.resyncAnimation)
{
this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetRange(target, IID_Attack, this.order.data.attackType))
{
this.SetNextState("COMBAT.CHASING");
return;
}
}
}
// if we're targetting a formation, find a new member of that formation
var cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
// if there is no target, it means previously searching for the target inside the target formation failed, so don't repeat the search
if (target && cmpTargetFormation)
{
this.order.data.target = this.order.data.formationTarget;
this.TimerHandler(msg.data, msg.lateness);
return;
}
this.oldAttackType = this.order.data.attackType;
// Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up
// Except if in WalkAndFight mode where we look for more ennemies around before moving again
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
// See if we can switch to a new nearby enemy
if (this.FindNewTargets())
{
// Attempt to immediately re-enter the timer function, to avoid wasting the attack.
if (this.orderQueue.length > 0 && this.orderQueue[0].data.attackType == this.oldAttackType)
this.TimerHandler(msg.data, msg.lateness);
return;
}
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
},
// TODO: respond to target deaths immediately, rather than waiting
// until the next Timer event
"Attacked": function(msg) {
if (this.order.data.target != msg.data.attacker)
{
// If we're attacked by a close enemy, stronger than our current target,
// we choose to attack it, but only if we're not forced to target something else
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force)))
{
var ents = [this.order.data.target, msg.data.attacker];
SortEntitiesByPriority(ents);
if (ents[0] != this.order.data.target)
{
this.RespondToTargetedEntities(ents);
}
}
}
},
},
"CHASING": {
"enter": function () {
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.SelectAnimation("move");
var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsFleeing())
{
// Run after a fleeing target
var speed = this.GetRunSpeed();
this.SetMoveSpeed(speed);
}
this.StartTimer(1000, 1000);
},
"HealthChanged": function() {
var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsFleeing())
return;
var speed = this.GetRunSpeed();
this.SetMoveSpeed(speed);
},
"leave": function() {
// Reset normal speed in case it was changed
this.SetMoveSpeed(this.GetWalkSpeed());
// Show carried resources when walking.
this.SetGathererAnimationOverride();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
this.SetNextState("ATTACKING");
},
},
},
"GATHER": {
"APPROACHING": {
"enter": function() {
this.SelectAnimation("move");
this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave".
// check that we can gather from the resource we're supposed to gather from.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
- if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
+ var cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage);
+ if ((!cmpMirage || !cmpMirage.ResourceSupply()) &&
+ (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity)))
{
// Save the current order's data in case we need it later
var oldType = this.order.data.type;
var oldTarget = this.order.data.target;
var oldTemplate = this.order.data.template;
// Try the next queued order if there is any
if (this.FinishOrder())
return true;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
ent != oldTarget
&& ((type.generic == "treasure" && oldType.generic == "treasure")
|| (type.specific == oldType.specific
&& (type.specific != "meat" || oldTemplate == template)))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return true;
}
else
{
// It's probably better in this case, to avoid units getting stuck around a dropsite
// in a "Target is far away, full, nearby are no good resources, return to dropsite" loop
// to order it to GatherNear the resource position.
var cmpPosition = Engine.QueryInterface(this.gatheringTarget, IID_Position);
if (cmpPosition)
{
var pos = cmpPosition.GetPosition();
this.GatherNearPosition(pos.x, pos.z, oldType, oldTemplate);
return true;
} else {
// we're kind of stuck here. Return resource.
var nearby = this.FindNearestDropsite(oldType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return true;
}
}
}
return true;
}
return false;
},
"MoveCompleted": function(msg) {
if (msg.data.error)
{
// We failed to reach the target
// remove us from the list of entities gathering from Resource.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply && cmpOwnership)
cmpSupply.RemoveGatherer(this.entity, cmpOwnership.GetOwner());
else if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
// Save the current order's data in case we need it later
var oldType = this.order.data.type;
var oldTarget = this.order.data.target;
var oldTemplate = this.order.data.template;
// Try the next queued order if there is any
if (this.FinishOrder())
return;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
ent != oldTarget
&& ((type.generic == "treasure" && oldType.generic == "treasure")
|| (type.specific == oldType.specific
&& (type.specific != "meat" || oldTemplate == template)))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// Couldn't find anything else. Just try this one again,
// maybe we'll succeed next time
this.PerformGather(oldTarget, false, false);
return;
}
// We reached the target - start gathering from it now
this.SetNextState("GATHERING");
},
"leave": function() {
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
},
},
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
this.SelectAnimation("move");
},
"MoveCompleted": function(msg) {
var resourceType = this.order.data.type;
var resourceTemplate = this.order.data.template;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
// If there is a nearby resource start gathering
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// Couldn't find nearby resources, so give up
if (this.FinishOrder())
return;
// Nothing better to do: go back to dropsite
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// No dropsites, just give up
},
},
"GATHERING": {
"enter": function() {
this.gatheringTarget = this.order.data.target; // deleted in "leave".
// Check if the resource is full.
if (this.gatheringTarget)
{
// Check that we can gather from the resource we're supposed to gather from.
// Will only be added if we're not already in.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
{
this.gatheringTarget = INVALID_ENTITY;
this.StartTimer(0);
return false;
}
}
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
this.order.data.force = false;
this.order.data.autoharvest = true;
// Calculate timing based on gather rates
// This allows the gather rate to control how often we gather, instead of how much.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget);
if (!rate)
{
// Try to find another target if the current one stopped existing
if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity))
{
// Let the Timer logic handle this
this.StartTimer(0);
return false;
}
// No rate, give up on gathering
this.FinishOrder();
return true;
}
// Scale timing interval based on rate, and start timer
// The offset should be at least as long as the repeat time so we use the same value for both.
var offset = 1000/rate;
var repeat = offset;
this.StartTimer(offset, repeat);
// We want to start the gather animation as soon as possible,
// but only if we're actually at the target and it's still alive
// (else it'll look like we're chopping empty air).
// (If it's not alive, the Timer handler will deal with sending us
// off to a different target.)
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
var typename = "gather_" + this.order.data.type.specific;
this.SelectAnimation(typename, false, 1.0, typename);
}
return false;
},
"leave": function() {
this.StopTimer();
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
// Show the carried resource, if we've gathered anything.
this.SetGathererAnimationOverride();
},
"Timer": function(msg) {
var resourceTemplate = this.order.data.template;
var resourceType = this.order.data.type;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply && cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity))
{
// Check we can still reach and gather from the target
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget))
{
// Gather the resources:
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Try to gather treasure
if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget))
return;
// If we've already got some resources but they're the wrong type,
// drop them first to ensure we're only ever carrying one type
if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
cmpResourceGatherer.DropResources();
// Collect from the target
var status = cmpResourceGatherer.PerformGather(this.gatheringTarget);
// If we've collected as many resources as possible,
// return to the nearest dropsite
if (status.filled)
{
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
// (Keep this Gather order on the stack so we'll
// continue gathering after returning)
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on gathering.
this.FinishOrder();
return;
}
// We can gather more from this target, do so in the next timer
if (!status.exhausted)
return;
}
else
{
// Try to follow the target
if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
this.SetNextState("APPROACHING");
return;
}
// Can't reach the target, or it doesn't exist any more
// We want to carry on gathering resources in the same area as
// the old one. So try to get close to the old resource's
// last known position
var maxRange = 8; // get close but not too close
if (this.order.data.lastPos &&
this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z,
0, maxRange))
{
this.SetNextState("APPROACHING");
return;
}
}
}
// We're already in range, can't get anywhere near it or the target is exhausted.
var herdPos = this.order.data.initPos;
// Give up on this order and try our next queued order
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
// Try to find a new resource of the same specific type near our current position:
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// If hunting, try to go to the initial herd position to see if we are more lucky
if (herdPos)
this.GatherNearPosition(herdPos.x, herdPos.z, resourceType, resourceTemplate);
// Nothing else to gather - if we're carrying anything then we should
// drop it off, and if not then we might as well head to the dropsite
// anyway because that's a nice enough place to congregate and idle
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// No dropsites - just give up
},
},
},
"HEAL": {
"Attacked": function(msg) {
// If we stand ground we will rather die than flee
if (!this.GetStance().respondStandGround && !this.order.data.force)
this.Flee(msg.data.attacker, false);
},
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
this.SetNextState("HEALING");
},
},
"HEALING": {
"enter": function() {
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
this.healTimers = cmpHeal.GetTimers();
// If the repeat time since the last heal hasn't elapsed,
// delay the action to avoid healing too fast.
var prepare = this.healTimers.prepare;
if (this.lastHealed)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
this.SelectAnimation("heal", false, 1.0, "heal");
this.SetAnimationSync(prepare, this.healTimers.repeat);
this.StartTimer(prepare, this.healTimers.repeat);
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = (prepare != this.healTimers.prepare) ? true : false;
this.FaceTowardsTarget(this.order.data.target);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
// Check the target is still alive and healable
if (this.TargetIsAlive(target) && this.CanHeal(target))
{
// Check if we can still reach the target
if (this.CheckTargetRange(target, IID_Heal))
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastHealed = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
cmpHeal.PerformHeal(target);
if (this.resyncAnimation)
{
this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetRange(target, IID_Heal))
{
this.SetNextState("HEAL.CHASING");
return;
}
}
}
// Can't reach it, healed to max hp or doesn't exist any more - give up
if (this.FinishOrder())
return;
// Heal another one
if (this.FindNewHealTargets())
return;
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
},
},
"CHASING": {
"enter": function () {
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function () {
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function () {
this.SetNextState("HEALING");
},
},
},
// Returning to dropsite
"RETURNRESOURCE": {
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with the carry animation after stopping moving
this.SelectAnimation("idle");
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
cmpResourceGatherer.CommitResources(dropsiteTypes);
// Stop showing the carried resource animation.
this.SetGathererAnimationOverride();
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
// The dropsite was destroyed, or we couldn't reach it, or ownership changed
// Look for a new one.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var genericType = cmpResourceGatherer.GetMainCarryingType();
var nearby = this.FindNearestDropsite(genericType);
if (nearby)
{
this.FinishOrder();
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on returning.
this.FinishOrder();
},
},
},
"TRADE": {
"Attacked": function(msg) {
// Ignore attack
// TODO: Inform player
},
"APPROACHINGFIRSTMARKET": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
if (this.waypoints && this.waypoints.length)
{
if (!this.MoveToMarket(this.order.data.firstMarket))
this.StopTrading();
}
else
this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "APPROACHINGSECONDMARKET");
},
},
"APPROACHINGSECONDMARKET": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
if (this.waypoints && this.waypoints.length)
{
if (!this.MoveToMarket(this.order.data.secondMarket))
this.StopTrading();
}
else
this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "APPROACHINGFIRSTMARKET");
},
},
},
"REPAIR": {
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.SetNextState("REPAIRING");
},
},
"REPAIRING": {
"enter": function() {
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
if (this.order.data.force)
this.order.data.autoharvest = true;
this.order.data.force = false;
this.repairTarget = this.order.data.target; // temporary, deleted in "leave".
// Check we can still reach and repair the target
if (!this.CanRepair(this.repairTarget))
{
// Can't reach it, no longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return true;
}
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
{
if (this.MoveToTargetRange(this.repairTarget, IID_Builder))
this.SetNextState("APPROACHING");
else
this.FinishOrder();
return true;
}
// Check if the target is still repairable
var cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints())
{
// The building was already finished/fully repaired before we arrived;
// let the ConstructionFinished handler handle this.
this.OnGlobalConstructionFinished({"entity": this.repairTarget, "newentity": this.repairTarget});
return true;
}
var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation);
if (cmpFoundation)
cmpFoundation.AddBuilder(this.entity);
this.SelectAnimation("build", false, 1.0, "build");
this.StartTimer(1000, 1000);
return false;
},
"leave": function() {
var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation);
if (cmpFoundation)
cmpFoundation.RemoveBuilder(this.entity);
delete this.repairTarget;
this.StopTimer();
},
"Timer": function(msg) {
// Check we can still reach and repair the target
if (!this.CanRepair(this.repairTarget))
{
// No longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return;
}
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
cmpBuilder.PerformBuilding(this.repairTarget);
// if the building is completed, the leave() function will be called
// by the ConstructionFinished message
// in that case, the repairTarget is deleted, and we can just return
if (!this.repairTarget)
return;
if (this.MoveToTargetRange(this.repairTarget, IID_Builder))
this.SetNextState("APPROACHING");
else if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
this.FinishOrder(); //can't approach and isn't in reach
},
},
"ConstructionFinished": function(msg) {
if (msg.data.entity != this.order.data.target)
return; // ignore other buildings
// Save the current order's data in case we need it later
var oldData = this.order.data;
// Save the current state so we can continue walking if necessary
// FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation.
// Idle animation while moving towards finished construction looks weird (ghosty).
var oldState = this.GetCurrentState();
// We finished building it.
// Switch to the next order (if any)
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
// If autocontinue explicitly disabled (e.g. by AI) then
// do nothing automatically
if (!oldData.autocontinue)
return;
// If this building was e.g. a farm of ours, the entities that recieved
// the build command should start gathering from it
if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity))
{
this.PerformGather(msg.data.newentity, true, false);
return;
}
// If this building was e.g. a farmstead of ours, entities that received
// the build command should look for nearby resources to gather
if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false))
{
var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
var types = cmpResourceDropsite.GetTypes();
// TODO: Slightly undefined behavior here, we don't know what type of resource will be collected,
// may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that!
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (types.indexOf(type.generic) != -1);
});
if (nearby)
{
this.PerformGather(nearby, true, false);
return;
}
}
// Look for a nearby foundation to help with
var nearbyFoundation = this.FindNearbyFoundation();
if (nearbyFoundation)
{
this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true);
return;
}
// Unit was approaching and there's nothing to do now, so switch to walking
if (oldState === "INDIVIDUAL.REPAIR.APPROACHING")
{
// We're already walking to the given point, so add this as a order.
this.WalkToTarget(msg.data.newentity, true);
}
},
},
"GARRISON": {
"enter": function() {
// If the garrisonholder should pickup, warn it so it can take needed action
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
},
"leave": function() {
// If a pickup has been requested and not yet canceled, cancel it
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"APPROACHING": {
"enter": function() {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
if(this.IsUnderAlert())
{
// check that we can garrison in the building we're supposed to garrison in
var cmpGarrisonHolder = Engine.QueryInterface(this.alertGarrisoningTarget, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
// Try to find another nearby building
var nearby = this.FindNearbyGarrisonHolder();
if (nearby)
{
this.alertGarrisoningTarget = nearby;
this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget});
}
else
this.FinishOrder();
}
else
this.SetNextState("GARRISONED");
}
else
this.SetNextState("GARRISONED");
},
},
"GARRISONED": {
"enter": function() {
// Target is not handled the same way with Alert and direct garrisoning
if(this.order.data.target)
var target = this.order.data.target;
else
{
if(!this.alertGarrisoningTarget)
{
// We've been unable to find a target nearby, so give up
this.FinishOrder();
return true;
}
var target = this.alertGarrisoningTarget;
}
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
// Check that we can garrison here
if (this.CanGarrison(target))
{
// Check that we're in range of the garrison target
if (this.CheckGarrisonRange(target))
{
// Check that garrisoning succeeds
if (cmpGarrisonHolder.Garrison(this.entity))
{
this.isGarrisoned = true;
if (this.formationController)
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
{
// disable rearrange for this removal,
// but enable it again for the next
// move command
var rearrange = cmpFormation.rearrange;
cmpFormation.SetRearrange(false);
cmpFormation.RemoveMembers([this.entity]);
cmpFormation.SetRearrange(rearrange);
}
}
// Check if we are garrisoned in a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
cmpResourceGatherer.CommitResources(dropsiteTypes);
this.SetGathererAnimationOverride();
}
}
// If a pickup has been requested, remove it
if (this.pickup)
{
var cmpHolderPosition = Engine.QueryInterface(target, IID_Position);
var cmpHolderUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderPosition)
cmpHolderUnitAI.lastShorelinePosition = cmpHolderPosition.GetPosition();
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
if (this.IsTurret())
this.SetNextState("IDLE");
return false;
}
}
else
{
// Unable to reach the target, try again (or follow if it is a moving target)
// except if the does not exits anymore or its orders have changed
if (this.pickup)
{
var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.HasPickupOrder(this.entity))
{
this.FinishOrder();
return true;
}
}
if (this.MoveToTarget(target))
{
this.SetNextState("APPROACHING");
return false;
}
}
}
// Garrisoning failed for some reason, so finish the order
this.FinishOrder();
return true;
},
"Order.Ungarrison": function() {
if (this.FinishOrder())
return;
},
"leave": function() {
this.isGarrisoned = false;
}
},
},
"AUTOGARRISON": {
"enter": function() {
this.isGarrisoned = true;
return false;
},
"Order.Ungarrison": function() {
if (this.FinishOrder())
return;
},
"leave": function() {
this.isGarrisoned = false;
}
},
"CHEERING": {
"enter": function() {
// Unit is invulnerable while cheering
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
this.SelectAnimation("promotion");
this.StartTimer(2800, 2800);
return false;
},
"leave": function() {
this.StopTimer();
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(false);
},
"Timer": function(msg) {
this.FinishOrder();
},
},
"PACKING": {
"enter": function() {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Pack();
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore attacks while packing
},
},
"UNPACKING": {
"enter": function() {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Unpack();
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore attacks while unpacking
},
},
"PICKUP": {
"APPROACHING": {
"enter": function() {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.SetNextState("LOADING");
},
"PickupCanceled": function() {
this.StopMoving();
this.FinishOrder();
},
},
"LOADING": {
"enter": function() {
this.SelectAnimation("idle");
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return true;
}
return false;
},
"PickupCanceled": function() {
this.FinishOrder();
},
},
},
},
"ANIMAL": {
"Attacked": function(msg) {
if (this.template.NaturalBehaviour == "skittish" ||
this.template.NaturalBehaviour == "passive")
{
this.Flee(msg.data.attacker, false);
}
else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive")
{
if (this.CanAttack(msg.data.attacker))
this.Attack(msg.data.attacker, false);
}
else if (this.template.NaturalBehaviour == "domestic")
{
// Never flee, stop what we were doing
this.SetNextState("IDLE");
}
},
"Order.LeaveFoundation": function(msg) {
// Run away from the foundation
this.FinishOrder();
this.PushOrderFront("Flee", { "target": msg.data.target, "force": false });
},
"IDLE": {
// (We need an IDLE state so that FinishOrder works)
"enter": function() {
// Start feeding immediately
this.SetNextState("FEEDING");
return true;
},
},
"ROAMING": {
"enter": function() {
// Walk in a random direction
this.SelectAnimation("walk", false, this.GetWalkSpeed());
this.MoveRandomly(+this.template.RoamDistance);
// Set a random timer to switch to feeding state
this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
this.SetFacePointAfterMove(false);
},
"leave": function() {
this.StopTimer();
this.SetFacePointAfterMove(true);
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.IsDangerousAnimal())
{
this.AttackVisibleEntity(msg.data.added);
}
// TODO: if two units enter our range together, we'll attack the
// first and then the second won't trigger another LosRangeUpdate
// so we won't notice it. Probably we should do something with
// ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to
// find any units that are already in range.
},
"Timer": function(msg) {
this.SetNextState("FEEDING");
},
"MoveCompleted": function() {
this.MoveRandomly(+this.template.RoamDistance);
},
},
"FEEDING": {
"enter": function() {
// Stop and eat for a while
this.SelectAnimation("feeding");
this.StopMoving();
this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
},
"leave": function() {
this.StopTimer();
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.template.NaturalBehaviour == "violent")
{
this.AttackVisibleEntity(msg.data.added);
}
},
"MoveCompleted": function() { },
"Timer": function(msg) {
this.SetNextState("ROAMING");
},
},
"FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals
"COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals
"WALKING": "INDIVIDUAL.WALKING", // reuse the same walking behaviour for animals
// only used for domestic animals
},
};
UnitAI.prototype.Init = function()
{
this.orderQueue = []; // current order is at the front of the list
this.order = undefined; // always == this.orderQueue[0]
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isGarrisoned = false;
this.isIdle = false;
this.lastFormationTemplate = "";
this.finishedOrder = false; // used to find if all formation members finished the order
this.heldPosition = undefined;
// Queue of remembered works
this.workOrders = [];
this.isGuardOf = undefined;
// "Town Bell" behaviour
this.alertRaiser = undefined;
this.alertGarrisoningTarget = undefined;
// For preventing increased action rate due to Stop orders or target death.
this.lastAttacked = undefined;
this.lastHealed = undefined;
this.SetStance(this.template.DefaultStance);
};
UnitAI.prototype.IsTurret = function()
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY;
};
UnitAI.prototype.ReactsToAlert = function(level)
{
return this.template.AlertReactiveLevel <= level;
};
UnitAI.prototype.IsUnderAlert = function()
{
return this.alertRaiser != undefined;
};
UnitAI.prototype.ResetAlert = function()
{
this.alertGarrisoningTarget = undefined;
this.alertRaiser = undefined;
};
UnitAI.prototype.GetAlertRaiser = function()
{
return this.alertRaiser;
};
UnitAI.prototype.IsFormationController = function()
{
return (this.template.FormationController == "true");
};
UnitAI.prototype.IsFormationMember = function()
{
return (this.formationController != INVALID_ENTITY);
};
UnitAI.prototype.HasFinishedOrder = function()
{
return this.finishedOrder;
};
UnitAI.prototype.ResetFinishOrder = function()
{
this.finishedOrder = false;
};
UnitAI.prototype.IsAnimal = function()
{
return (this.template.NaturalBehaviour ? true : false);
};
UnitAI.prototype.IsDangerousAnimal = function()
{
return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" ||
this.template.NaturalBehaviour == "aggressive"));
};
UnitAI.prototype.IsDomestic = function()
{
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (!cmpIdentity)
return false;
return cmpIdentity.HasClass("Domestic");
};
UnitAI.prototype.IsHealer = function()
{
return Engine.QueryInterface(this.entity, IID_Heal);
};
UnitAI.prototype.IsIdle = function()
{
return this.isIdle;
};
UnitAI.prototype.IsGarrisoned = function()
{
return this.isGarrisoned || this.IsTurret();
};
UnitAI.prototype.IsFleeing = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "FLEEING");
};
UnitAI.prototype.IsWalking = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "WALKING");
};
/**
* return true if in WalkAndFight looking for new targets
*/
UnitAI.prototype.IsWalkingAndFighting = function()
{
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
return (cmpUnitAI && cmpUnitAI.IsWalkingAndFighting());
}
return (this.orderQueue.length > 0 && this.orderQueue[0].type == "WalkAndFight");
};
UnitAI.prototype.OnCreate = function()
{
if (this.IsAnimal())
this.UnitFsm.Init(this, "ANIMAL.FEEDING");
else if (this.IsFormationController())
this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
else
this.UnitFsm.Init(this, "INDIVIDUAL.IDLE");
};
UnitAI.prototype.OnDiplomacyChanged = function(msg)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
this.SetupRangeQuery();
};
UnitAI.prototype.OnOwnershipChanged = function(msg)
{
this.SetupRangeQueries();
// If the unit isn't being created or dying, reset stance and clear orders (if not garrisoned).
if (msg.to != -1 && msg.from != -1)
{
// Switch to a virgin state to let states execute their leave handlers.
var index = this.GetCurrentState().indexOf(".");
if (index != -1)
this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index));
this.SetStance(this.template.DefaultStance);
if(!this.isGarrisoned)
this.Stop(false);
}
};
UnitAI.prototype.OnDestroy = function()
{
// Switch to an empty state to let states execute their leave handlers.
this.UnitFsm.SwitchToNextState(this, "");
// Clean up range queries
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
rangeMan.DestroyActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
};
UnitAI.prototype.OnVisionRangeChanged = function(msg)
{
// Update range queries
if (this.entity == msg.entity)
this.SetupRangeQueries();
};
UnitAI.prototype.HasPickupOrder = function(entity)
{
for each (var order in this.orderQueue)
if (order.type == "PickupUnit" && order.data.target == entity)
return true;
return false;
};
UnitAI.prototype.OnPickupRequested = function(msg)
{
// First check if we already have such a request
if (this.HasPickupOrder(msg.entity))
return;
// Otherwise, insert the PickUp order after the last forced order
this.PushOrderAfterForced("PickupUnit", { "target": msg.entity });
};
UnitAI.prototype.OnPickupCanceled = function(msg)
{
var cmpUnitAI = Engine.QueryInterface(msg.entity, IID_UnitAI);
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type == "PickupUnit" && this.orderQueue[i].data.target == msg.entity)
{
if (i == 0)
this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg});
else
this.orderQueue.splice(i, 1);
break;
}
}
};
// Wrapper function that sets up the normal and healer range queries.
UnitAI.prototype.SetupRangeQueries = function()
{
this.SetupRangeQuery();
if (this.IsHealer())
this.SetupHealRangeQuery();
}
// Set up a range query for all enemy and gaia units within LOS range
// which can be attacked.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupRangeQuery = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (this.losRangeQuery)
{
rangeMan.DestroyActiveQuery(this.losRangeQuery);
this.losRangeQuery = undefined;
}
var players = [];
if (owner != -1)
{
// If unit not just killed, get enemy players via diplomacy
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
var numPlayers = playerMan.GetNumPlayers();
for (var i = 0; i < numPlayers; ++i)
{
// Exclude allies, and self
// TODO: How to handle neutral players - Special query to attack military only?
if (cmpPlayer.IsEnemy(i))
players.push(i);
}
}
var range = this.GetQueryRange(IID_Attack);
this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, rangeMan.GetEntityFlagMask("normal"));
rangeMan.EnableActiveQuery(this.losRangeQuery);
};
// Set up a range query for all own or ally units within LOS range
// which can be healed.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupHealRangeQuery = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (this.losHealRangeQuery)
rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
var players = [owner];
if (owner != -1)
{
// If unit not just killed, get ally players via diplomacy
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
var numPlayers = playerMan.GetNumPlayers();
for (var i = 1; i < numPlayers; ++i)
{
// Exclude gaia and enemies
if (cmpPlayer.IsAlly(i))
players.push(i);
}
}
var range = this.GetQueryRange(IID_Heal);
this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, rangeMan.GetEntityFlagMask("injured"));
rangeMan.EnableActiveQuery(this.losHealRangeQuery);
};
//// FSM linkage functions ////
UnitAI.prototype.SetNextState = function(state)
{
this.UnitFsm.SetNextState(this, state);
};
// This will make sure that the state is always entered even if this means leaving it and reentering it
// This is so that a state can be reinitialized with new order data without having to switch to an intermediate state
UnitAI.prototype.SetNextStateAlwaysEntering = function(state)
{
this.UnitFsm.SetNextStateAlwaysEntering(this, state);
};
UnitAI.prototype.DeferMessage = function(msg)
{
this.UnitFsm.DeferMessage(this, msg);
};
UnitAI.prototype.GetCurrentState = function()
{
return this.UnitFsm.GetCurrentState(this);
};
UnitAI.prototype.FsmStateNameChanged = function(state)
{
Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state });
};
/**
* Call when the current order has been completed (or failed).
* Removes the current order from the queue, and processes the
* next one (if any). Returns false and defaults to IDLE
* if there are no remaining orders.
*/
UnitAI.prototype.FinishOrder = function()
{
if (!this.orderQueue.length)
{
var stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(this.entity);
error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
}
this.orderQueue.shift();
this.order = this.orderQueue[0];
if (this.orderQueue.length)
{
var ret = this.UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
{
return this.FinishOrder();
}
// Otherwise we've successfully processed a new order
return true;
}
else
{
this.SetNextState("IDLE");
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// Check if there are queued formation orders
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
// Inform the formation controller that we finished this task
this.finishedOrder = true;
// We don't want to carry out the default order
// if there are still queued formation orders left
if (cmpUnitAI.GetOrders().length > 1)
return true;
}
}
return false;
}
};
/**
* Add an order onto the back of the queue,
* and execute it if we didn't already have an order.
*/
UnitAI.prototype.PushOrder = function(type, data)
{
var order = { "type": type, "data": data };
this.orderQueue.push(order);
// If we didn't already have an order, then process this new one
if (this.orderQueue.length == 1)
{
this.order = order;
var ret = this.UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
this.FinishOrder();
}
};
/**
* Add an order onto the front of the queue,
* and execute it immediately.
*/
UnitAI.prototype.PushOrderFront = function(type, data)
{
var order = { "type": type, "data": data };
// If current order is cheering then add new order after it
// same thing if current order if packing/unpacking
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue.unshift(cheeringOrder, order);
}
else if (this.order && this.IsPacking())
{
var packingOrder = this.orderQueue.shift();
this.orderQueue.unshift(packingOrder, order);
}
else
{
this.orderQueue.unshift(order);
this.order = order;
var ret = this.UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off again;
// assume the previous active order is still valid (the short-lived
// new order hasn't changed state or anything) so we can carry on
// as if nothing had happened
if (ret && ret.discardOrder)
{
this.orderQueue.shift();
this.order = this.orderQueue[0];
}
}
};
/**
* Insert an order after the last forced order onto the queue
* and after the other orders of the same type
*/
UnitAI.prototype.PushOrderAfterForced = function(type, data)
{
if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type))
{
this.PushOrderFront(type, data);
}
else
{
for (var i = 1; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
continue;
if (this.orderQueue[i].type == type)
continue;
this.orderQueue.splice(i, 0, {"type": type, "data": data});
return;
}
this.PushOrder(type, data);
}
};
UnitAI.prototype.ReplaceOrder = function(type, data)
{
// Remember the previous work orders to be able to go back to them later if required
if (data && data.force)
{
if (this.IsFormationController())
this.CallMemberFunction("UpdateWorkOrders", [type]);
else
this.UpdateWorkOrders(type);
}
// Special cases of orders that shouldn't be replaced:
// 1. Cheering - we're invulnerable, add order after we finish
// 2. Packing/unpacking - we're immobile, add order after we finish (unless it's cancel)
// TODO: maybe a better way of doing this would be to use priority levels
if (this.order && this.order.type == "Cheering")
{
var order = { "type": type, "data": data };
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [cheeringOrder, order];
}
else if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack")
{
var order = { "type": type, "data": data };
var packingOrder = this.orderQueue.shift();
this.orderQueue = [packingOrder, order];
}
else
{
this.orderQueue = [];
this.PushOrder(type, data);
}
};
UnitAI.prototype.GetOrders = function()
{
return this.orderQueue.slice();
};
UnitAI.prototype.AddOrders = function(orders)
{
for each (var order in orders)
{
this.PushOrder(order.type, order.data);
}
};
UnitAI.prototype.GetOrderData = function()
{
var orders = [];
for (var i in this.orderQueue)
{
if (this.orderQueue[i].data)
orders.push(deepcopy(this.orderQueue[i].data));
}
return orders;
};
UnitAI.prototype.UpdateWorkOrders = function(type)
{
// Under alert, remembered work orders won't be forgotten
if (this.IsUnderAlert())
return;
var isWorkType = function(type){
return (type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource");
};
// If we are being re-affected to a work order, forget the previous ones
if (isWorkType(type))
{
this.workOrders = [];
return;
}
// Then if we already have work orders, keep them
if (this.workOrders.length)
return;
// First if the unit is in a formation, get its workOrders from it
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i)
{
if (isWorkType(cmpUnitAI.orderQueue[i].type))
{
this.workOrders = cmpUnitAI.orderQueue.slice(i);
return;
}
}
}
}
// If nothing found, take the unit orders
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (isWorkType(this.orderQueue[i].type))
{
this.workOrders = this.orderQueue.slice(i);
return;
}
}
};
UnitAI.prototype.BackToWork = function()
{
if (this.workOrders.length == 0)
return false;
// Clear the order queue considering special orders not to avoid
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [cheeringOrder];
}
else
this.orderQueue = [];
this.AddOrders(this.workOrders);
// And if the unit is in a formation, remove it from the formation
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers([this.entity]);
}
this.workOrders = [];
return true;
};
UnitAI.prototype.HasWorkOrders = function()
{
return this.workOrders.length > 0;
};
UnitAI.prototype.GetWorkOrders = function()
{
return this.workOrders;
};
UnitAI.prototype.SetWorkOrders = function(orders)
{
this.workOrders = orders;
};
UnitAI.prototype.TimerHandler = function(data, lateness)
{
// Reset the timer
if (data.timerRepeat === undefined)
{
this.timer = undefined;
}
this.UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};
/**
* Set up the UnitAI timer to run after 'offset' msecs, and then
* every 'repeat' msecs until StopTimer is called. A "Timer" message
* will be sent each time the timer runs.
*/
UnitAI.prototype.StartTimer = function(offset, repeat)
{
if (this.timer)
error("Called StartTimer when there's already an active timer");
var data = { "timerRepeat": repeat };
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (repeat === undefined)
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
else
this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data);
};
/**
* Stop the current UnitAI timer.
*/
UnitAI.prototype.StopTimer = function()
{
if (!this.timer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
};
//// Message handlers /////
UnitAI.prototype.OnMotionChanged = function(msg)
{
if (msg.starting && !msg.error)
{
this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
}
else if (!msg.starting || msg.error)
{
this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
}
};
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
{
// TODO: This is a bit inefficient since every unit listens to every
// construction message - ideally we could scope it to only the one we're building
this.UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
};
UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
for each (var order in this.orderQueue)
{
if (order.data && order.data.target && order.data.target == msg.entity)
order.data.target = msg.newentity;
if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity)
order.data.formationTarget = msg.newentity;
}
if (this.isGuardOf && this.isGuardOf == msg.entity)
this.isGuardOf = msg.newentity;
};
UnitAI.prototype.OnAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
};
UnitAI.prototype.OnGuardedAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
};
UnitAI.prototype.OnHealthChanged = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
};
UnitAI.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag == this.losRangeQuery)
this.UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
else if (msg.tag == this.losHealRangeQuery)
this.UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg});
};
UnitAI.prototype.OnPackFinished = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
};
//// Helper functions to be called by the FSM ////
UnitAI.prototype.GetWalkSpeed = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.GetWalkSpeed();
};
UnitAI.prototype.GetRunSpeed = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
var runSpeed = cmpUnitMotion.GetRunSpeed();
var walkSpeed = cmpUnitMotion.GetWalkSpeed();
if (runSpeed <= walkSpeed)
return runSpeed;
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var health = cmpHealth.GetHitpoints()/cmpHealth.GetMaxHitpoints();
return (health*runSpeed + (1-health)*walkSpeed);
};
/**
* Returns true if the target exists and has non-zero hitpoints.
*/
UnitAI.prototype.TargetIsAlive = function(ent)
{
var cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
return true;
+ var cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
+ if (cmpMirage)
+ return true;
+
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (!cmpHealth)
return false;
return (cmpHealth.GetHitpoints() != 0);
};
/**
* Returns true if the target exists and needs to be killed before
* beginning to gather resources from it.
*/
UnitAI.prototype.MustKillGatherTarget = function(ent)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
if (!cmpResourceSupply.GetKillBeforeGather())
return false;
return this.TargetIsAlive(ent);
};
/**
* Returns the entity ID of the nearest resource supply where the given
* filter returns true, or undefined if none can be found.
* TODO: extend this to exclude resources that already have lots of
* gatherers.
*/
UnitAI.prototype.FindNearbyResource = function(filter)
{
var range = 64; // TODO: what's a sensible number?
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
// We accept resources owned by Gaia or any player
var players = [0];
for (var i = 1; i < playerMan.GetNumPlayers(); ++i)
players.push(i);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply);
for each (var ent in nearby)
{
if (!this.CanGather(ent))
continue;
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
var amount = cmpResourceSupply.GetCurrentAmount();
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
// Remove "resource|" prefix from template names, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
if (amount > 0 && cmpResourceSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity) && filter(ent, type, template))
return ent;
}
return undefined;
};
/**
* Returns the entity ID of the nearest resource dropsite that accepts
* the given type, or undefined if none can be found.
*/
UnitAI.prototype.FindNearestDropsite = function(genericType)
{
// Find dropsites owned by this unit's player
var players = [];
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
players = [cmpOwnership.GetOwner()];
// Ships are unable to reach land dropsites and shouldn't attempt to do so.
var excludeLand = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship");
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
if (excludeLand)
{
nearby = nearby.filter( function(e) {
return Engine.QueryInterface(e, IID_Identity).HasClass("Naval");
});
}
for each (var ent in nearby)
{
var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (!cmpDropsite.AcceptsType(genericType))
continue;
return ent;
}
return undefined;
};
/**
* Returns the entity ID of the nearest building that needs to be constructed,
* or undefined if none can be found close enough.
*/
UnitAI.prototype.FindNearbyFoundation = function()
{
var range = 64; // TODO: what's a sensible number?
// Find buildings owned by this unit's player
var players = [];
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
players = [cmpOwnership.GetOwner()];
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_Foundation);
for each (var ent in nearby)
{
// Skip foundations that are already complete. (This matters since
// we process the ConstructionFinished message before the foundation
// we're working on has been deleted.)
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation.IsFinished())
continue;
return ent;
}
return undefined;
};
/**
* Returns the entity ID of the nearest building in which the unit can garrison,
* or undefined if none can be found close enough.
*/
UnitAI.prototype.FindNearbyGarrisonHolder = function()
{
var range = 128; // TODO: what's a sensible number?
// Find buildings owned by this unit's player
var players = [];
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
players = [cmpOwnership.GetOwner()];
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_GarrisonHolder);
for each (var ent in nearby)
{
var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
// We only want to garrison in buildings, not in moving units like ships,...
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI && cmpGarrisonHolder.AllowedToGarrison(this.entity) && !cmpGarrisonHolder.IsFull())
return ent;
}
return undefined;
};
/**
* Play a sound appropriate to the current entity.
*/
UnitAI.prototype.PlaySound = function(name)
{
// If we're a formation controller, use the sounds from our first member
if (this.IsFormationController())
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
var member = cmpFormation.GetPrimaryMember();
if (member)
PlaySound(name, member);
}
else
{
// Otherwise use our own sounds
PlaySound(name, this.entity);
}
};
UnitAI.prototype.SetGathererAnimationOverride = function(disable)
{
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return;
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
// Remove the animation override, so that weapons are shown again.
if (disable)
{
cmpVisual.ResetMoveAnimation("walk");
return;
}
// Work out what we're carrying, in order to select an appropriate animation
var type = cmpResourceGatherer.GetLastCarriedType();
if (type)
{
var typename = "carry_" + type.generic;
// Special case for meat
if (type.specific == "meat")
typename = "carry_" + type.specific;
cmpVisual.ReplaceMoveAnimation("walk", typename);
}
else
cmpVisual.ResetMoveAnimation("walk");
}
UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
// Special case: the "move" animation gets turned into a special
// movement mode that deals with speeds and walk/run automatically
if (name == "move")
{
// Speed to switch from walking to running animations
var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2;
cmpVisual.SelectMovementAnimation(runThreshold);
return;
}
var soundgroup;
if (sound)
{
var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
soundgroup = cmpSound.GetSoundGroup(sound);
}
// Set default values if unspecified
if (once === undefined)
once = false;
if (speed === undefined)
speed = 1.0;
if (soundgroup === undefined)
soundgroup = "";
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
};
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetAnimationSyncRepeat(repeattime);
cmpVisual.SetAnimationSyncOffset(actiontime);
};
UnitAI.prototype.StopMoving = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.StopMoving();
};
UnitAI.prototype.MoveToPoint = function(x, z)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, 0, 0);
};
UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
};
UnitAI.prototype.MoveToTarget = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
};
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
if (!this.CheckTargetVisible(target) || this.IsTurret())
return false;
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Move unit so we hope the target is in the attack range
* for melee attacks, this goes straight to the default range checks
* for ranged attacks, the parabolic range is used
*/
UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
var cmpFormationAttack = Engine.QueryInterface(this.formationController, IID_Attack);
var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationAttack && cmpFormationAttack.CanAttackAsFormation() && cmpFormationUnitAI && cmpFormationUnitAI.GetCurrentState() == "FORMATIONCONTROLLER.ATTACKING")
return false;
}
var cmpFormation = Engine.QueryInterface(target, IID_Formation)
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if(type!= "Ranged")
return this.MoveToTargetRange(target, IID_Attack, type);
if (!this.CheckTargetVisible(target))
return false;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(type);
var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
var s = thisCmpPosition.GetPosition();
var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if(!targetCmpPosition.IsInWorld())
return false;
var t = targetCmpPosition.GetPosition();
// h is positive when I'm higher than the target
var h = s.y-t.y+range.elevationBonus;
// No negative roots please
if(h>-range.max/2)
var parabolicMaxRange = Math.sqrt(range.max*range.max+2*range.max*h);
else
// return false? Or hope you come close enough?
var parabolicMaxRange = 0;
//return false;
// the parabole changes while walking, take something in the middle
var guessedMaxRange = (range.max + parabolicMaxRange)/2;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange))
return true;
// if that failed, try closer
return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange));
};
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, min, max);
};
UnitAI.prototype.MoveToGarrisonRange = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInPointRange(x, z, min, max);
};
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
};
/**
* Check if the target is inside the attack range
* For melee attacks, this goes straigt to the regular range calculation
* For ranged attacks, the parabolic formula is used to accout for bigger ranges
* when the target is lower, and smaller ranges when the target is higher
*/
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
var cmpFormationAttack = Engine.QueryInterface(this.formationController, IID_Attack);
var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if
(
cmpFormationAttack &&
cmpFormationAttack.CanAttackAsFormation() &&
cmpFormationUnitAI &&
cmpFormationUnitAI.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING" &&
cmpFormationUnitAI.order.data.target == target
)
return true;
}
var cmpFormation = Engine.QueryInterface(target, IID_Formation)
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
return this.CheckTargetRange(target, IID_Attack, type);
var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
return false;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(type);
var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
var s = thisCmpPosition.GetPosition();
var t = targetCmpPosition.GetPosition();
var h = s.y-t.y+range.elevationBonus;
var maxRangeSq = 2*range.max*(h + range.max/2);
if (maxRangeSq < 0)
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq));
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, min, max);
};
UnitAI.prototype.CheckGarrisonRange = function(target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2)
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
UnitAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
return false;
+ // Entities that are hidden and miraged are considered visible
+ var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
+ if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
+ return true;
+
if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden")
return false;
// Either visible directly, or visible in fog
return true;
};
UnitAI.prototype.FaceTowardsTarget = function(target)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
var targetpos = cmpTargetPosition.GetPosition();
var angle = Math.atan2(targetpos.x - pos.x, targetpos.z - pos.z);
var rot = cmpPosition.GetRotation();
var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI;
if (Math.abs(delta) > 0.2)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.z);
}
};
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type);
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return false;
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var halfvision = cmpVision.GetRange() / 2;
var pos = cmpPosition.GetPosition();
var heldPosition = this.heldPosition;
if (heldPosition === undefined)
heldPosition = {"x": pos.x, "z": pos.z};
var dx = heldPosition.x - pos.x;
var dz = heldPosition.z - pos.z;
var dist = Math.sqrt(dx*dx + dz*dz);
return dist < halfvision + range.max;
};
UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var range = cmpVision.GetRange();
var distance = DistanceBetweenEntities(this.entity,target);
return distance < range;
};
UnitAI.prototype.GetBestAttack = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttack();
};
UnitAI.prototype.GetBestAttackAgainst = function(target)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttackAgainst(target);
};
UnitAI.prototype.GetAttackBonus = function(type, target)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return 1;
return cmpAttack.GetAttackBonus(type, target);
};
/**
* Try to find one of the given entities which can be attacked,
* and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackVisibleEntity = function(ents, forceResponse)
{
for each (var target in ents)
{
if (this.CanAttack(target, forceResponse))
{
this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse });
return true;
}
}
return false;
};
/**
* Try to find one of the given entities which can be attacked
* and which is close to the hold position, and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackEntityInZone = function(ents, forceResponse)
{
for each (var target in ents)
{
var type = this.GetBestAttackAgainst(target);
if (this.CanAttack(target, forceResponse) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, type)
&& (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target)))
{
this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse });
return true;
}
}
return false;
};
/**
* Try to respond appropriately given our current stance,
* given a list of entities that match our stance's target criteria.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToTargetedEntities = function(ents)
{
if (!ents.length)
return false;
if (this.GetStance().respondChase)
return this.AttackVisibleEntity(ents, true);
if (this.GetStance().respondStandGround)
return this.AttackVisibleEntity(ents, true);
if (this.GetStance().respondHoldGround)
return this.AttackEntityInZone(ents, true);
if (this.GetStance().respondFlee)
{
this.PushOrderFront("Flee", { "target": ents[0], "force": false });
return true;
}
return false;
};
/**
* Try to respond to healable entities.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToHealableEntities = function(ents)
{
if (!ents.length)
return false;
for each (var ent in ents)
{
if (this.CanHeal(ent))
{
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
}
}
return false;
};
/**
* Returns true if we should stop following the target entity.
*/
UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type)
{
// Forced orders shouldn't be interrupted.
if (force)
return false;
// If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
var cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack)
{
for each (var targetType in cmpAttack.GetAttackTypes())
if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, targetType))
return false;
}
}
// Stop if we're in hold-ground mode and it's too far from the holding point
if (this.GetStance().respondHoldGround)
{
if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type))
return true;
}
// Stop if it's left our vision range, unless we're especially persistent
if (!this.GetStance().respondChaseBeyondVision)
{
if (!this.CheckTargetIsInVisionRange(target))
return true;
}
// (Note that CCmpUnitMotion will detect if the target is lost in FoW,
// and will continue moving to its last seen position and then stop)
return false;
};
/*
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
if (this.IsTurret())
return false;
// TODO: use special stances instead?
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack)
return false;
if (this.GetStance().respondChase)
return true;
// If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
var cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack)
{
for each (var type in cmpAttack.GetAttackTypes())
if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type))
return true;
}
}
if (force)
return true;
return false;
};
//// External interface functions ////
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
cmpObstruction.SetControlGroup(this.entity);
else
cmpObstruction.SetControlGroup(ent);
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};
UnitAI.prototype.GetFormationController = function()
{
return this.formationController;
};
UnitAI.prototype.SetLastFormationTemplate = function(template)
{
this.lastFormationTemplate = template;
};
UnitAI.prototype.GetLastFormationTemplate = function()
{
return this.lastFormationTemplate;
};
UnitAI.prototype.MoveIntoFormation = function(cmd)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
// Add new order to move into formation at the current position
this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
};
UnitAI.prototype.GetTargetPositions = function()
{
var targetPositions = [];
for (var i = 0; i < this.orderQueue.length; ++i)
{
var order = this.orderQueue[i];
switch (order.type)
{
case "Walk":
case "WalkAndFight":
case "WalkToPointRange":
case "MoveIntoFormation":
case "GatherNearPosition":
targetPositions.push(new Vector2D(order.data.x, order.data.z));
break; // and continue the loop
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Guard":
case "Flee":
case "LeaveFoundation":
case "Attack":
case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
case "Garrison":
// Find the target unit's position
var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return targetPositions;
targetPositions.push(cmpTargetPosition.GetPosition2D());
return targetPositions;
case "Stop":
return [];
default:
error("GetTargetPositions: Unrecognised order type '"+order.type+"'");
return [];
}
}
return targetPositions;
};
/**
* Returns the estimated distance that this unit will travel before either
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
* Intended for Formation to switch to column layout on long walks.
*/
UnitAI.prototype.ComputeWalkingDistance = function()
{
var distance = 0;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return 0;
// Keep track of the position at the start of each order
var pos = cmpPosition.GetPosition2D();
var targetPositions = this.GetTargetPositions();
for (var i = 0; i < targetPositions.length; i++)
{
distance += pos.distanceTo(targetPositions[i]);
// Remember this as the start position for the next order
pos = targetPositions[i];
}
// Return the total distance to the end of the order queue
return distance;
};
UnitAI.prototype.AddOrder = function(type, data, queued)
{
if (this.expectedRoute)
this.expectedRoute = undefined;
if (queued)
this.PushOrder(type, data);
else
this.ReplaceOrder(type, data);
};
/**
* Adds guard/escort order to the queue, forced by the player.
*/
UnitAI.prototype.Guard = function(target, queued)
{
if (!this.CanGuard())
{
this.WalkToTarget(target, queued);
return;
}
// if we already had an old guard order, do nothing if the target is the same
// and the order is running, otherwise remove the previous order
if (this.isGuardOf)
{
if (this.isGuardOf == target && this.order && this.order.type == "Guard")
return;
else
this.RemoveGuard();
}
this.AddOrder("Guard", { "target": target, "force": false }, queued);
};
UnitAI.prototype.AddGuard = function(target)
{
if (!this.CanGuard())
return false;
var cmpGuard = Engine.QueryInterface(target, IID_Guard);
if (!cmpGuard)
return false;
// Do not allow to guard a unit already guarding
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsGuardOf())
return false;
this.isGuardOf = target;
this.guardRange = cmpGuard.GetRange(this.entity);
cmpGuard.AddGuard(this.entity);
return true;
};
UnitAI.prototype.RemoveGuard = function()
{
if (this.isGuardOf)
{
var cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
if (cmpGuard)
cmpGuard.RemoveGuard(this.entity);
this.guardRange = undefined;
this.isGuardOf = undefined;
}
if (!this.order)
return;
if (this.order.type == "Guard")
this.UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"});
else
for (var i = 1; i < this.orderQueue.length; ++i)
if (this.orderQueue[i].type == "Guard")
this.orderQueue.splice(i, 1);
};
UnitAI.prototype.IsGuardOf = function()
{
return this.isGuardOf;
};
UnitAI.prototype.SetGuardOf = function(entity)
{
// entity may be undefined
this.isGuardOf = entity;
};
UnitAI.prototype.CanGuard = function()
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Do not let a unit already guarded to guard. This would work in principle,
// but would clutter the gui with too much buttons to take all cases into account
var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
if (cmpGuard && cmpGuard.GetEntities().length)
return false;
return (this.template.CanGuard == "true");
};
/**
* Adds walk order to queue, forced by the player.
*/
UnitAI.prototype.Walk = function(x, z, queued)
{
if (this.expectedRoute && queued)
this.expectedRoute.push({ "x": x, "z": z });
else
this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued);
};
/**
* Adds walk to point range order to queue, forced by the player.
*/
UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued)
{
this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued);
};
/**
* Adds stop order to queue, forced by the player.
*/
UnitAI.prototype.Stop = function(queued)
{
this.AddOrder("Stop", undefined, queued);
};
/**
* Adds walk-to-target order to queue, this only occurs in response
* to a player order, and so is forced.
*/
UnitAI.prototype.WalkToTarget = function(target, queued)
{
this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued);
};
/**
* Adds walk-and-fight order to queue, this only occurs in response
* to a player order, and so is forced.
*/
UnitAI.prototype.WalkAndFight = function(x, z, queued)
{
this.AddOrder("WalkAndFight", { "x": x, "z": z, "force": true }, queued);
};
/**
* Adds leave foundation order to queue, treated as forced.
*/
UnitAI.prototype.LeaveFoundation = function(target)
{
// If we're already being told to leave a foundation, then
// ignore this new request so we don't end up being too indecisive
// to ever actually move anywhere
// Ignore also the request if we are packing
if (this.order && (this.order.type == "LeaveFoundation" || this.IsPacking()))
return;
this.PushOrderFront("LeaveFoundation", { "target": target, "force": true });
};
/**
* Adds attack order to the queue, forced by the player.
*/
UnitAI.prototype.Attack = function(target, queued)
{
if (!this.CanAttack(target))
{
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
this.MoveToTargetRange(target, IID_Heal);
else
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Attack", { "target": target, "force": true }, queued);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.Garrison = function(target, queued)
{
if (target == this.entity)
return;
if (!this.CanGarrison(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true }, queued);
};
/**
* Adds ungarrison order to the queue.
*/
UnitAI.prototype.Ungarrison = function()
{
if (this.IsGarrisoned())
{
this.AddOrder("Ungarrison", null, false);
}
};
/**
* Adds autogarrison order to the queue (only used by ProductionQueue for auto-garrisoning
* and Promotion when promoting already garrisoned entities).
*/
UnitAI.prototype.Autogarrison = function(target)
{
this.AddOrder("Autogarrison", { "target": target }, false);
};
/**
* Adds gather order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Gather = function(target, queued)
{
this.PerformGather(target, queued, true);
};
/**
* Internal function to abstract the force parameter.
*/
UnitAI.prototype.PerformGather = function(target, queued, force)
{
if (!this.CanGather(target))
{
this.WalkToTarget(target, queued);
return;
}
// Save the resource type now, so if the resource gets destroyed
// before we process the order then we still know what resource
// type to look for more of
+ var type;
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
- var type = cmpResourceSupply.GetType();
+ var cmpMirage = Engine.QueryInterface(target, IID_Mirage);
+ if (cmpResourceSupply)
+ type = cmpResourceSupply.GetType();
+ else if (cmpMirage && cmpMirage.ResourceSupply())
+ type = cmpMirage.GetType();
+ else
+ error("CanGather allowed gathering from invalid entity");
// Also save the target entity's template, so that if it's an animal,
// we won't go from hunting slow safe animals to dangerous fast ones
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(target);
// Remove "resource|" prefix from template name, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
// Remember the position of our target, if any, in case it disappears
// later and we want to head to its last known position
var lastPos = undefined;
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
lastPos = cmpPosition.GetPosition();
this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued);
};
/**
* Adds gather-near-position order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued)
{
// Remove "resource|" prefix from template name, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued);
};
/**
* Adds heal order to the queue, forced by the player.
*/
UnitAI.prototype.Heal = function(target, queued)
{
if (!this.CanHeal(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Heal", { "target": target, "force": true }, queued);
};
/**
* Adds return resource order to the queue, forced by the player.
*/
UnitAI.prototype.ReturnResource = function(target, queued)
{
if (!this.CanReturnResource(target, true))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
};
/**
* Adds trade order to the queue. Either walk to the first market, or
* start a new route. Not forced, so it can be interrupted by attacks.
* The possible route may be given directly as a SetupTradeRoute argument
* if coming from a RallyPoint, or through this.expectedRoute if a user command.
*/
UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued)
{
if (!this.CanTrade(target))
{
this.WalkToTarget(target, queued);
return;
}
var marketsChanged = this.SetTargetMarket(target, source);
if (marketsChanged)
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader.HasBothMarkets())
{
var data = { "firstMarket": cmpTrader.GetFirstMarket(), "secondMarket": cmpTrader.GetSecondMarket(), "route": route, "force": false };
if (this.expectedRoute)
{
if (!route && this.expectedRoute.length)
data.route = this.expectedRoute.slice();
this.expectedRoute = undefined;
}
if (this.IsFormationController())
{
this.CallMemberFunction("AddOrder", ["Trade", data, queued]);
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.Disband();
}
else
this.AddOrder("Trade", data, queued);
}
else
{
if (this.IsFormationController())
this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]);
else
this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
this.expectedRoute = [];
}
}
};
UnitAI.prototype.SetTargetMarket = function(target, source)
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader)
return false;
var marketsChanged = cmpTrader.SetTargetMarket(target, source);
if (this.IsFormationController())
this.CallMemberFunction("SetTargetMarket", [target, source]);
return marketsChanged;
};
UnitAI.prototype.MoveToMarket = function(targetMarket)
{
if (this.waypoints && this.waypoints.length > 1)
{
var point = this.waypoints.pop();
var ok = this.MoveToPoint(point.x, point.z);
if (!ok)
ok = this.MoveToMarket(targetMarket);
}
else
{
this.waypoints = undefined;
var ok = this.MoveToTarget(targetMarket);
}
return ok;
};
UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket, nextMarket, nextFsmStateName)
{
if (!this.CanTrade(currentMarket))
{
this.StopTrading();
return;
}
if (this.CheckTargetRange(currentMarket, IID_Trader))
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.PerformTrade(currentMarket);
if (!cmpTrader.GetGain().traderGain)
{
this.StopTrading();
return;
}
if (this.order.data.route && this.order.data.route.length)
{
this.waypoints = this.order.data.route.slice();
if (nextFsmStateName == "APPROACHINGSECONDMARKET")
this.waypoints.reverse();
this.waypoints.unshift(null); // additionnal dummy point for the market
}
if (this.MoveToMarket(nextMarket)) // We've started walking to the next market
this.SetNextState(nextFsmStateName);
else
this.StopTrading();
}
else
{
if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again
this.StopTrading();
}
};
UnitAI.prototype.StopTrading = function()
{
this.StopMoving();
this.FinishOrder();
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.StopTrading();
};
/**
* Adds repair/build order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Repair = function(target, autocontinue, queued)
{
if (!this.CanRepair(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued);
};
/**
* Adds flee order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.Flee = function(target, queued)
{
this.AddOrder("Flee", { "target": target, "force": false }, queued);
};
/**
* Adds cheer order to the queue. Forced so it won't be interrupted by attacks.
*/
UnitAI.prototype.Cheer = function()
{
this.AddOrder("Cheering", { "force": true }, false);
};
UnitAI.prototype.Pack = function(queued)
{
// Check that we can pack
if (this.CanPack())
this.AddOrder("Pack", { "force": true }, queued);
};
UnitAI.prototype.Unpack = function(queued)
{
// Check that we can unpack
if (this.CanUnpack())
this.AddOrder("Unpack", { "force": true }, queued);
};
UnitAI.prototype.CancelPack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
this.AddOrder("CancelPack", { "force": true }, queued);
};
UnitAI.prototype.CancelUnpack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
this.AddOrder("CancelUnpack", { "force": true }, queued);
};
UnitAI.prototype.SetStance = function(stance)
{
if (g_Stances[stance])
this.stance = stance;
else
error("UnitAI: Setting to invalid stance '"+stance+"'");
};
UnitAI.prototype.SwitchToStance = function(stance)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
this.SetStance(stance);
// Stop moving if switching to stand ground
// TODO: Also stop existing orders in a sensible way
if (stance == "standground")
this.StopMoving();
// Reset the range queries, since the range depends on stance.
this.SetupRangeQueries();
};
/**
* Resets losRangeQuery, and if there are some targets in range that we can
* attack then we start attacking and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewTargets = function()
{
if (!this.losRangeQuery)
return false;
if (!this.GetStance().targetVisibleEnemies)
return false;
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.AttackEntitiesByPreference( rangeMan.ResetActiveQuery(this.losRangeQuery) ))
return true;
return false;
};
UnitAI.prototype.FindWalkAndFightTargets = function()
{
if (this.IsFormationController())
{
var cmpUnitAI;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
for each (var ent in cmpFormation.members)
{
if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI)))
continue;
var targets = cmpUnitAI.GetTargetsFromUnit();
for each (var targ in targets)
{
if (cmpUnitAI.CanAttack(targ))
{
this.PushOrderFront("Attack", { "target": targ, "force": true });
return true;
}
}
}
return false;
}
var targets = this.GetTargetsFromUnit();
for each (var targ in targets)
{
if (this.CanAttack(targ))
{
this.PushOrderFront("Attack", { "target": targ, "force": true });
return true;
}
}
return false;
};
UnitAI.prototype.GetTargetsFromUnit = function()
{
if (!this.losRangeQuery)
return [];
if (!this.GetStance().targetVisibleEnemies)
return [];
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return [];
const attackfilter = function(e) {
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var entities = rangeMan.ResetActiveQuery(this.losRangeQuery);
var targets = entities.filter(function (v) { return cmpAttack.CanAttack(v) && attackfilter(v); })
.sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); });
return targets;
};
/**
* Resets losHealRangeQuery, and if there are some targets in range that we can heal
* then we start healing and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewHealTargets = function()
{
if (!this.losHealRangeQuery)
return false;
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
for each (var ent in ents)
{
if (this.CanHeal(ent))
{
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
}
}
// We haven't found any target to heal
return false;
};
UnitAI.prototype.GetQueryRange = function(iid)
{
var ret = { "min": 0, "max": 0 };
if (this.GetStance().respondStandGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
ret.min = range.min;
ret.max = range.max;
}
else if (this.GetStance().respondChase)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
else if (this.GetStance().respondHoldGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var halfvision = cmpVision.GetRange() / 2;
ret.max = range.max + halfvision;
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid === IID_Heal)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
return ret;
};
UnitAI.prototype.GetStance = function()
{
return g_Stances[this.stance];
};
UnitAI.prototype.GetPossibleStances = function()
{
return Object.keys(g_Stances);
};
UnitAI.prototype.GetStanceName = function()
{
return this.stance;
};
UnitAI.prototype.SetMoveSpeed = function(speed)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.SetSpeed(speed);
};
UnitAI.prototype.SetHeldPosition = function(x, z)
{
this.heldPosition = {"x": x, "z": z};
};
UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
};
UnitAI.prototype.GetHeldPosition = function()
{
return this.heldPosition;
};
UnitAI.prototype.WalkToHeldPosition = function()
{
if (this.heldPosition)
{
this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false);
return true;
}
return false;
};
//// Helper functions ////
UnitAI.prototype.CanAttack = function(target, forceResponse)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Attack commands
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
if (!cmpAttack.CanAttack(target))
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
// Verify that the target is an attackable resource supply like a domestic animal
// or that it isn't owned by an ally of this entity's player or is responding to
// an attack.
var owner = cmpOwnership.GetOwner();
if (!this.MustKillGatherTarget(target)
&& !(IsOwnedByEnemyOfPlayer(owner, target)
|| IsOwnedByNeutralOfPlayer(owner, target)
|| (forceResponse && !IsOwnedByPlayer(owner, target))))
return false;
return true;
};
UnitAI.prototype.CanGarrison = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
// Verify that the target is owned by this entity's player or a mutual ally of this player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Don't let animals garrison for now
// (If we want to support that, we'll need to change Order.Garrison so it
// doesn't move the animal into an INVIDIDUAL.* state)
if (this.IsAnimal())
return false;
return true;
};
UnitAI.prototype.CanGather = function(target)
{
if (this.IsTurret())
return false;
- // The target must be a valid resource supply.
+ // The target must be a valid resource supply, or the mirage of one.
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
- if (!cmpResourceSupply)
+ var cmpMirage = Engine.QueryInterface(target, IID_Mirage);
+ if (!cmpResourceSupply && !(cmpMirage && cmpMirage.ResourceSupply()))
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that we can gather from this target
if (!cmpResourceGatherer.GetTargetGatherRate(target))
return false;
// No need to verify ownership as we should be able to gather from
// a target regardless of ownership.
// No need to call "cmpResourceSupply.IsAvailable()" either because that
// would cause units to walk to full entities instead of choosing another one
// nearby to gather from, which is undesirable.
return true;
};
UnitAI.prototype.CanHeal = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Heal commands
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
if (!cmpHeal)
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
// Verify that the target is owned by the same player as the entity or of an ally
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Verify that the target is not unhealable (or at max health)
var cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.IsUnhealable())
return false;
// Verify that the target has no unhealable class
var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetUnhealableClasses()))
return false;
// Verify that the target is a healable class
if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetHealableClasses()))
return true;
return false;
};
UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to ReturnResource commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that the target is a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return false;
if (checkCarriedResource)
{
// Verify that we are carrying some resources,
// and can return our current resource to this target
var type = cmpResourceGatherer.GetMainCarryingType();
if (!type || !cmpResourceDropsite.AcceptsType(type))
return false;
}
// Verify that the dropsite is owned by this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
return false;
return true;
};
UnitAI.prototype.CanTrade = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Trade commands
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader || !cmpTrader.CanTrade(target))
return false;
return true;
};
UnitAI.prototype.CanRepair = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Repair (Builder) commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
return false;
// Verify that the target is owned by an ally of this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))
return false;
return true;
};
UnitAI.prototype.CanPack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return (cmpPack && !cmpPack.IsPacking() && !cmpPack.IsPacked());
};
UnitAI.prototype.CanUnpack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return (cmpPack && !cmpPack.IsPacking() && cmpPack.IsPacked());
};
UnitAI.prototype.IsPacking = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return (cmpPack && cmpPack.IsPacking());
};
//// Animal specific functions ////
UnitAI.prototype.MoveRandomly = function(distance)
{
// We want to walk in a random direction, but avoid getting stuck
// in obstacles or narrow spaces.
// So pick a circular range from approximately our current position,
// and move outwards to the nearest point on that circle, which will
// lead to us avoiding obstacles and moving towards free space.
// TODO: we probably ought to have a 'home' point, and drift towards
// that, so we don't spread out all across the whole map
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition)
return;
if (!cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
var jitter = 0.5;
// Randomly adjust the range's center a bit, so we tend to prefer
// moving in random directions (if there's nothing in the way)
var tx = pos.x + (2*Math.random()-1)*jitter;
var tz = pos.z + (2*Math.random()-1)*jitter;
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.MoveToPointRange(tx, tz, distance, distance);
};
UnitAI.prototype.SetFacePointAfterMove = function(val)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpMotion)
cmpMotion.SetFacePointAfterMove(val);
};
UnitAI.prototype.AttackEntitiesByPreference = function(ents)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
const attackfilter = function(e) {
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
return this.RespondToTargetedEntities(
ents.filter(function (v) { return cmpAttack.CanAttack(v) && attackfilter(v); })
.sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); })
);
};
/**
* Call obj.funcname(args) on UnitAI components of all formation members.
*/
UnitAI.prototype.CallMemberFunction = function(funcname, args)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
var members = cmpFormation.GetMembers();
for each (var ent in members)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI[funcname].apply(cmpUnitAI, args);
}
};
/**
* Call obj.functname(args) on UnitAI components of all formation members,
* and return true if all calls return true.
*/
UnitAI.prototype.TestAllMemberFunction = function(funcname, args)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return false;
var members = cmpFormation.GetMembers();
for each (var ent in members)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI[funcname].apply(cmpUnitAI, args))
return false;
}
return true;
};
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Messages.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Messages.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Messages.js (revision 15612)
@@ -1,10 +1,11 @@
/**
* Broadcast message
* sent when one entity is changed to other:
- * from Foundation component when building constuction is done
- * and from Promotion component when unit is promoted
+ * - from Foundation component when building construction is done
+ * - from Promotion component when unit is promoted
+ * - from Mirage component when a fogged entity is re-discovered
* Data: { entity: , newentity: }
*/
Engine.RegisterMessageType("EntityRenamed");
Engine.RegisterMessageType("DiplomacyChanged");
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 15612)
@@ -1,435 +1,437 @@
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/RallyPoint.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js")
Engine.LoadComponentScript("interfaces/Trader.js")
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
GetPrices: function() { return {
"buy": { "food": 150 },
"sell": { "food": 25 },
}},
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
GetGameType: function() { return "conquest"; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetNumPlayers: function() { return 2; },
GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; },
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
GetLosVisibility: function(ent, player) { return "visible"; },
GetLosCircular: function() { return false; },
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
GetCurrentTemplateName: function(ent) { return "example"; },
GetTemplate: function(name) { return ""; },
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
GetTime: function() { return 0; },
SetTimeout: function(ent, iid, funcname, time, data) { return 0; },
});
AddMock(100, IID_Player, {
GetName: function() { return "Player 1"; },
GetCiv: function() { return "gaia"; },
GetColour: function() { return { r: 1, g: 1, b: 1, a: 1}; },
GetPopulationCount: function() { return 10; },
GetPopulationLimit: function() { return 20; },
GetMaxPopulation: function() { return 200; },
GetResourceCounts: function() { return { food: 100 }; },
GetHeroes: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() { return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
GetConquestCriticalEntitiesCount: function() { return 1; },
IsAlly: function() { return false; },
IsMutualAlly: function() { return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return true; },
});
AddMock(100, IID_EntityLimits, {
GetLimits: function() { return {"Foo": 10}; },
GetCounts: function() { return {"Foo": 5}; },
GetLimitChangers: function() {return {"Foo": {}}; }
});
AddMock(100, IID_TechnologyManager, {
IsTechnologyResearched: function(tech) { if (tech == "phase_village") return true; else return false; },
GetQueuedResearch: function() { return {}; },
GetStartedResearch: function() { return {}; },
GetResearchedTechs: function() { return {}; },
GetClassCounts: function() { return {}; },
GetTypeCountsByClass: function() { return {}; },
GetTechModifications: function() { return {}; },
});
AddMock(100, IID_StatisticsTracker, {
GetStatistics: function() {
return {
"unitsTrained": 10,
"unitsLost": 9,
"buildingsConstructed": 5,
"buildingsLost": 4,
"civCentresBuilt": 1,
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0,
},
"treasuresCollected": 0,
"percentMapExplored": 10,
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; },
});
AddMock(101, IID_Player, {
GetName: function() { return "Player 2"; },
GetCiv: function() { return "celt"; },
GetColour: function() { return { r: 1, g: 0, b: 0, a: 1}; },
GetPopulationCount: function() { return 40; },
GetPopulationLimit: function() { return 30; },
GetMaxPopulation: function() { return 300; },
GetResourceCounts: function() { return { food: 200 }; },
GetHeroes: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() {return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
GetConquestCriticalEntitiesCount: function() { return 1; },
IsAlly: function() { return true; },
IsMutualAlly: function() {return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return false; },
});
AddMock(101, IID_EntityLimits, {
GetLimits: function() { return {"Bar": 20}; },
GetCounts: function() { return {"Bar": 0}; },
GetLimitChangers: function() {return {"Bar": {}}; }
});
AddMock(101, IID_TechnologyManager, {
IsTechnologyResearched: function(tech) { if (tech == "phase_village") return true; else return false; },
GetQueuedResearch: function() { return {}; },
GetStartedResearch: function() { return {}; },
GetResearchedTechs: function() { return {}; },
GetClassCounts: function() { return {}; },
GetTypeCountsByClass: function() { return {}; },
GetTechModifications: function() { return {}; },
});
AddMock(101, IID_StatisticsTracker, {
GetStatistics: function() {
return {
"unitsTrained": 10,
"unitsLost": 9,
"buildingsConstructed": 5,
"buildingsLost": 4,
"civCentresBuilt": 1,
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0,
},
"treasuresCollected": 0,
"percentMapExplored": 10,
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; },
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
players: [
{
name: "Player 1",
civ: "gaia",
colour: { r:1, g:1, b:1, a:1 },
popCount: 10,
popLimit: 20,
popMax: 200,
heroes: [],
resourceCounts: { food: 100 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
phase: "village",
isAlly: [false, false],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [true, true],
entityLimits: {"Foo": 10},
entityCounts: {"Foo": 5},
entityLimitChangers: {"Foo": {}},
researchQueued: {},
researchStarted: {},
researchedTechs: {},
classCounts: {},
typeCountsByClass: {},
},
{
name: "Player 2",
civ: "celt",
colour: { r:1, g:0, b:0, a:1 },
popCount: 40,
popLimit: 30,
popMax: 300,
heroes: [],
resourceCounts: { food: 200 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
phase: "village",
isAlly: [true, true],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [false, false],
entityLimits: {"Bar": 20},
entityCounts: {"Bar": 0},
entityLimitChangers: {"Bar": {}},
researchQueued: {},
researchStarted: {},
researchedTechs: {},
classCounts: {},
typeCountsByClass: {},
}
],
circularMap: false,
timeElapsed: 0,
gameType: "conquest",
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
players: [
{
name: "Player 1",
civ: "gaia",
colour: { r:1, g:1, b:1, a:1 },
popCount: 10,
popLimit: 20,
popMax: 200,
heroes: [],
resourceCounts: { food: 100 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
phase: "village",
isAlly: [false, false],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [true, true],
entityLimits: {"Foo": 10},
entityCounts: {"Foo": 5},
entityLimitChangers: {"Foo": {}},
researchQueued: {},
researchStarted: {},
researchedTechs: {},
classCounts: {},
typeCountsByClass: {},
statistics: {
unitsTrained: 10,
unitsLost: 9,
buildingsConstructed: 5,
buildingsLost: 4,
civCentresBuilt: 1,
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0,
},
treasuresCollected: 0,
percentMapExplored: 10,
},
},
{
name: "Player 2",
civ: "celt",
colour: { r:1, g:0, b:0, a:1 },
popCount: 40,
popLimit: 30,
popMax: 300,
heroes: [],
resourceCounts: { food: 200 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
phase: "village",
isAlly: [true, true],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [false, false],
entityLimits: {"Bar": 20},
entityCounts: {"Bar": 0},
entityLimitChangers: {"Bar": {}},
researchQueued: {},
researchStarted: {},
researchedTechs: {},
classCounts: {},
typeCountsByClass: {},
statistics: {
unitsTrained: 10,
unitsLost: 9,
buildingsConstructed: 5,
buildingsLost: 4,
civCentresBuilt: 1,
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0,
},
treasuresCollected: 0,
percentMapExplored: 10,
},
}
],
circularMap: false,
timeElapsed: 0,
gameType: "conquest",
barterPrices: {buy: {food: 150}, sell: {food: 25}}
});
AddMock(10, IID_Builder, {
GetEntitiesList: function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
GetHitpoints: function() { return 50; },
GetMaxHitpoints: function() { return 60; },
IsRepairable: function() { return false; },
IsUnhealable: function() { return false; },
});
AddMock(10, IID_Identity, {
GetClassesList: function() { return ["class1", "class2"]; },
GetVisibleClassesList: function() { return ["class3", "class4"]; },
GetRank: function() { return "foo"; },
GetSelectionGroupName: function() { return "Selection Group Name"; },
HasClass: function() { return true; },
});
AddMock(10, IID_Position, {
GetTurretParent: function() {return INVALID_ENTITY;},
GetPosition: function() {
return {x:1, y:2, z:3};
},
GetRotation: function() {
return {x:4, y:5, z:6};
},
IsInWorld: function() {
return true;
},
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
id: 10,
template: "example",
alertRaiser: null,
buildEntities: ["test1", "test2"],
identity: {
rank: "foo",
classes: ["class1", "class2"],
visibleClasses: ["class3", "class4"],
selectionGroupName: "Selection Group Name",
},
+ fogging: null,
foundation: null,
garrisonHolder: null,
gate: null,
guard: null,
pack: null,
player: -1,
position: {x:1, y:2, z:3},
production: null,
rallyPoint: null,
rotation: {x:4, y:5, z:6},
trader: null,
unitAI: null,
visibility: "visible",
hitpoints: 50,
maxHitpoints: 60,
needsRepair: false,
needsHeal: true,
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedEntityState(-1, 10), {
armour: null,
attack: null,
barterMarket: {
prices: { "buy": {"food":150}, "sell": {"food":25} },
},
buildingAI: null,
healer: null,
+ mirage: null,
obstruction: null,
turretParent: null,
promotion: null,
resourceCarrying: null,
resourceDropsite: null,
resourceGatherRates: null,
resourceSupply: null,
});
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 15612)
@@ -1,1594 +1,1599 @@
// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
function ProcessCommand(player, cmd)
{
// Do some basic checks here that commanding player is valid
var data = {};
data.cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!data.cmpPlayerMan || player < 0)
return;
data.playerEnt = data.cmpPlayerMan.GetPlayerByID(player);
if (data.playerEnt == INVALID_ENTITY)
return;
data.cmpPlayer = Engine.QueryInterface(data.playerEnt, IID_Player);
if (!data.cmpPlayer)
return;
data.controlAllUnits = data.cmpPlayer.CanControlAllUnits();
if (cmd.entities)
data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// Now handle various commands
if (commands[cmd.type])
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("PlayerCommand", {"player": player, "cmd": cmd});
commands[cmd.type](player, cmd, data);
}
else
error("Invalid command: unknown command type: "+uneval(cmd));
}
var commands = {
"debug-print": function(player, cmd, data)
{
print(cmd.message);
},
"chat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": cmd.type, "players": [player], "message": cmd.message});
},
"aichat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": cmd.type, "players": [player], "message": cmd.message});
},
"cheat": function(player, cmd, data)
{
Cheat(cmd);
},
"quit": function(player, cmd, data)
{
// Let the AI exit the game for testing purposes
// TODO broken, does this need a fix?
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "quit"});
},
"diplomacy": function(player, cmd, data)
{
switch(cmd.to)
{
case "ally":
data.cmpPlayer.SetAlly(cmd.player);
break;
case "neutral":
data.cmpPlayer.SetNeutral(cmd.player);
break;
case "enemy":
data.cmpPlayer.SetEnemy(cmd.player);
break;
default:
warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "diplomacy", "players": [player], "player1": cmd.player, "status": cmd.to});
},
"tribute": function(player, cmd, data)
{
data.cmpPlayer.TributeResource(cmd.player, cmd.amounts);
},
"control-all": function(player, cmd, data)
{
data.cmpPlayer.SetControlAllUnits(cmd.flag);
},
"reveal-map": function(player, cmd, data)
{
// Reveal the map for all players, not just the current player,
// primarily to make it obvious to everyone that the player is cheating
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
},
"walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
});
},
"walk-to-range": function(player, cmd, data)
{
// Only used by the AI
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(cmpUnitAI)
cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued);
}
},
"attack-walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.queued);
});
},
"attack": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanAttack for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Attack(cmd.target, cmd.queued);
});
},
"heal": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
}
// See UnitAI.CanHeal for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Heal(cmd.target, cmd.queued);
});
},
"repair": function(player, cmd, data)
{
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanRepair for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
});
},
"gather": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
}
// See UnitAI.CanGather for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Gather(cmd.target, cmd.queued);
});
},
"gather-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued);
});
},
"returnresource": function(player, cmd, data)
{
// Check dropsite is owned by player
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
}
// See UnitAI.CanReturnResource for target checks
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
});
},
"back-to-work": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(!cmpUnitAI || !cmpUnitAI.BackToWork())
notifyBackToWorkFailure(player);
}
},
"remove-guard": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(cmpUnitAI)
cmpUnitAI.RemoveGuard();
}
},
"train": function(player, cmd, data)
{
// Check entity limits
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(cmd.template);
var unitCategory = null;
if (template.TrainingRestrictions)
unitCategory = template.TrainingRestrictions.Category;
// Verify that the building(s) can be controlled by the player
if (data.entities.length <= 0)
{
if (g_DebugCommands)
warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
for each (var ent in data.entities)
{
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));
continue;
}
}
var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
if (!cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
continue;
}
var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
// Check if the building can train the unit
// TODO: the AI API does not take promotion technologies into account for the list
// of trainable units (taken directly from the unit template). Here is a temporary fix.
if (queue && data.cmpPlayer.IsAI())
{
var list = queue.GetEntitiesList();
if (list.indexOf(cmd.template) === -1 && cmd.promoted)
{
for (var promoted of cmd.promoted)
{
if (list.indexOf(promoted) === -1)
continue;
cmd.template = promoted;
break;
}
}
}
if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
if ("metadata" in cmd)
queue.AddBatch(cmd.template, "unit", +cmd.count, cmd.metadata);
else
queue.AddBatch(cmd.template, "unit", +cmd.count);
}
},
"research": function(player, cmd, data)
{
// Verify that the building can be controlled by the player
if (!CanControlUnit(cmd.entity, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: research building cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
if (!cmpTechnologyManager.CanResearch(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.AddBatch(cmd.template, "technology");
},
"stop-production": function(player, cmd, data)
{
// Verify that the building can be controlled by the player
if (!CanControlUnit(cmd.entity, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: production building cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.RemoveBatch(cmd.id);
},
"construct": function(player, cmd, data)
{
TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"construct-wall": function(player, cmd, data)
{
TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"delete-entities": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply || !cmpResourceSupply.GetKillBeforeGather())
cmpHealth.Kill();
}
else
Engine.DestroyEntity(ent);
}
},
"set-rallypoint": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
if (!cmd.queued)
cmpRallyPoint.Unset();
cmpRallyPoint.AddPosition(cmd.x, cmd.z);
cmpRallyPoint.AddData(cmd.data);
}
}
},
"unset-rallypoint": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Reset();
}
},
"defeat-player": function(player, cmd, data)
{
// Send "OnPlayerDefeated" message to player
Engine.PostMessage(data.playerEnt, MT_PlayerDefeated, { "playerId": player } );
},
"garrison": function(player, cmd, data)
{
// Verify that the building can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Garrison(cmd.target, cmd.queued);
});
},
"guard": function(player, cmd, data)
{
// Verify that the target can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Guard(cmd.target, cmd.queued);
});
},
"stop": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.Stop(cmd.queued);
});
},
"unload": function(player, cmd, data)
{
// Verify that the building can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
var notUngarrisoned = 0;
// The owner can ungarrison every garrisoned unit
if (IsOwnedByPlayer(player, cmd.garrisonHolder))
data.entities = cmd.entities;
for each (var ent in data.entities)
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
notUngarrisoned++;
if (notUngarrisoned != 0)
notifyUnloadFailure(player, cmd.garrisonHolder)
},
"unload-template": function(player, cmd, data)
{
var index = cmd.template.indexOf("&"); // Templates for garrisoned units are extended
if (index == -1)
return;
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
// Only the owner of the garrisonHolder may unload entities from any owners
if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits
&& player != +cmd.template.slice(1,index))
continue;
if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.all))
notifyUnloadFailure(player, garrisonHolder);
}
}
},
"unload-all-own": function(player, cmd, data)
{
var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllOwn())
notifyUnloadFailure(player, garrisonHolder)
}
},
"unload-all": function(player, cmd, data)
{
var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
for each (var garrisonHolder in entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
notifyUnloadFailure(player, garrisonHolder)
}
},
"increase-alert-level": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (!cmpAlertRaiser || !cmpAlertRaiser.IncreaseAlertLevel())
notifyAlertFailure(player);
}
},
"alert-end": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.EndOfAlert();
}
},
"formation": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd.name).forEach(function(cmpUnitAI) {
cmpUnitAI.MoveIntoFormation(cmd);
});
},
"promote": function(player, cmd, data)
{
// No need to do checks here since this is a cheat anyway
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "chat", "players": [player], "message": "(Cheat - promoted units)"});
for each (var ent in cmd.entities)
{
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
}
},
"stance": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SwitchToStance(cmd.name);
}
},
"wall-to-gate": function(player, cmd, data)
{
for each (var ent in data.entities)
{
TryTransformWallToGate(ent, data.cmpPlayer, cmd.template);
}
},
"lock-gate": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
{
if (cmd.lock)
cmpGate.LockGate();
else
cmpGate.UnlockGate();
}
}
},
"setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) {
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
});
},
"select-required-goods": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
cmpTrader.SetRequiredGoods(cmd.requiredGoods);
}
},
"set-trading-goods": function(player, cmd, data)
{
data.cmpPlayer.SetTradingGoods(cmd.tradingGoods);
},
"barter": function(player, cmd, data)
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
cmpBarter.ExchangeResources(data.playerEnt, cmd.sell, cmd.buy, cmd.amount);
},
"set-shading-color": function(player, cmd, data)
{
// Debug command to make an entity brightly colored
for each (var ent in cmd.entities)
{
var cmpVisual = Engine.QueryInterface(ent, IID_Visual)
if (cmpVisual)
cmpVisual.SetShadingColour(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0) // alpha isn't used so just send 0
}
},
"pack": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmd.pack)
cmpUnitAI.Pack(cmd.queued);
else
cmpUnitAI.Unpack(cmd.queued);
}
}
},
"cancel-pack": function(player, cmd, data)
{
for each (var ent in data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmd.pack)
cmpUnitAI.CancelPack(cmd.queued);
else
cmpUnitAI.CancelUnpack(cmd.queued);
}
}
},
"dialog-answer": function(player, cmd, data)
{
// Currently nothing. Triggers can read it anyway, and send this
// message to any component you like.
},
};
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player, garrisonHolder)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Unable to ungarrison unit(s)" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Sends a GUI notification about worker(s) that failed to go back to work.
*/
function notifyBackToWorkFailure(player)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Some unit(s) can't go back to work" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Sends a GUI notification about Alerts that failed to be raised
*/
function notifyAlertFailure(player)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "You can't raise the alert to a higher level !" };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
/**
* Get some information about the formations used by entities.
* The entities must have a UnitAI component.
*/
function ExtractFormations(ents)
{
var entities = []; // subset of ents that have UnitAI
var members = {}; // { formationentity: [ent, ent, ...], ... }
for each (var ent in ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var fid = cmpUnitAI.GetFormationController();
if (fid != INVALID_ENTITY)
{
if (!members[fid])
members[fid] = [];
members[fid].push(ent);
}
entities.push(ent);
}
var ids = [ id for (id in members) ];
return { "entities": entities, "members": members, "ids": ids };
}
/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
function GetDockAngle(template, x, z)
{
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
return undefined;
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
else if (template.Footprint.Circle)
halfSize = template.Footprint.Circle["@radius"];
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = x - d*Math.sin(angle);
var nz = z + d*Math.cos(angle);
if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
waterPoints.push(i);
}
var consec = [];
var length = waterPoints.length;
if (!length)
continue;
for (var i = 0; i < length; ++i)
{
var count = 0;
for (var j = 0; j < (length-1); ++j)
{
if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length])
++count;
else
break;
}
consec[i] = count;
}
var start = 0;
var count = 0;
for (var c in consec)
{
if (consec[c] > count)
{
start = c;
count = consec[c];
}
}
// If we've found a shoreline, stop searching
if (count != numPoints-1)
return -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI);
}
return undefined;
}
/**
* Attempts to construct a building using the specified parameters.
* Returns true on success, false on failure.
*/
function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
{
// Message structure:
// {
// "type": "construct",
// "entities": [...], // entities that will be ordered to construct the building (if applicable)
// "template": "...", // template name of the entity being constructed
// "x": ...,
// "z": ...,
// "angle": ...,
// "metadata": "...", // AI metadata of the building
// "actorSeed": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
// "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
// "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. May be INVALID_ENTITY.
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check whether we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
return false;
var foundationTemplate = "foundation|" + cmd.template;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity(foundationTemplate);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
return false;
}
// If it's a dock, get the right angle.
var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateMgr.GetTemplate(cmd.template);
if (template.BuildRestrictions.Category === "Dock")
{
var angle = GetDockAngle(template, cmd.x, cmd.z);
if (angle !== undefined)
cmd.angle = angle;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(cmd.angle);
// Set the obstruction control group if needed
if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
{
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
// primary control group must always be valid
if (cmd.obstructionControlGroup)
{
if (cmd.obstructionControlGroup <= 0)
warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
}
if (cmd.obstructionControlGroup2)
cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (cmpBuildRestrictions)
{
var ret = cmpBuildRestrictions.CheckPlacement();
if (!ret.success)
{
if (g_DebugCommands)
{
warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
ret.players = [player];
cmpGuiInterface.PushNotification(ret);
// Remove the foundation because the construction was aborted
// move it out of world because it's not destroyed immediately.
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
}
else
error("cmpBuildRestrictions not defined");
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (!cmpEntityLimits || !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
{
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
}
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
// TODO: Enable this check once the AI gets technology support
if (!cmpTechnologyManager.CanProduce(cmd.template) && !cmpPlayer.IsAI())
{
if (g_DebugCommands)
{
warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "players": [player], "message": "Building's technology requirements are not met." });
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
}
// We need the cost after tech modifications
// To calculate this with an entity requires ownership, so use the template instead
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(foundationTemplate);
var costs = {};
for (var r in template.Cost.Resources)
{
costs[r] = +template.Cost.Resources[r];
if (cmpTechnologyManager)
costs[r] = cmpTechnologyManager.ApplyModificationsTemplate("Cost/Resources/"+r, costs[r], template);
}
if (!cmpPlayer.TrySubtractResources(costs))
{
if (g_DebugCommands)
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(ent);
cmpPosition.MoveOutOfWorld();
return false;
}
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual && cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// send Metadata info if any
if (cmd.metadata)
Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued
});
}
+
+ // Load a mirage for the owner of this new entity
+ var cmpFogging = Engine.QueryInterface(ent, IID_Fogging)
+ if (cmpFogging)
+ cmpFogging.LoadMirage(player);
return ent;
}
function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
{
// 'cmd' message structure:
// {
// "type": "construct-wall",
// "entities": [...], // entities that will be ordered to construct the wall (if applicable)
// "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
// {
// "template": "...", // one of the templates from the wallset
// "x": ...,
// "z": ...,
// "angle": ...,
// },
// ...
// ],
// "wallSet": {
// "templates": {
// "tower": // tower template name
// "long": // long wall segment template name
// ... // etc.
// },
// "maxTowerOverlap": ...,
// "minTowerOverlap": ...,
// },
// "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall
// "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
// }
if (cmd.pieces.length <= 0)
return;
if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
return;
}
if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
return;
}
// Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
// and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
// groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
// towers in the case of snapping). The towers themselves all keep their default unique control groups.
// To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
// it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
// first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
// the first tower encountered towards the ending side of the wall (if any).
// We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
// wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
//
// FIRST PASS:
// - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
// cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
// as the primary control group, thus allowing it to be built overlapping the previous piece.
// - If we encounter a new tower along the way (which will gain its own control group), do the following:
// o First build it using temporarily the same control group of the previous (non-tower) piece
// o Set the previous piece's secondary control group to the tower's entity ID
// o Restore the primary control group of the constructed tower back its original (unique) value.
// The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
// to be placed while overlapping the previous piece.
//
// SECOND PASS:
// - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
// time registering the right neighbouring tower in each non-tower piece.
// first pass; L -> R
var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
// the first wall piece can be built while overlapping it.
if (cmd.startSnappedEntity)
{
var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
if (!cmpSnappedStartObstruction)
{
error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
//warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
}
var i = 0;
for (; i < cmd.pieces.length; ++i)
{
var piece = cmd.pieces[i];
// All wall pieces after the first must be queued.
if (i > 0 && !cmd.queued)
cmd.queued = true;
// 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
// start position snapping (implying that the first entity we build must be a tower)
if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
{
if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
{
error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
break;
}
}
var constructPieceCmd = {
"type": "construct",
"entities": cmd.entities,
"template": piece.template,
"x": piece.x,
"z": piece.z,
"angle": piece.angle,
"autorepair": cmd.autorepair,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued,
// Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
// using the control group of the last tower (see comments above).
"obstructionControlGroup": lastTowerControlGroup,
};
// If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
// control group directly at construction time (instead of setting it in the second pass) to allow it to be built
// while overlapping the snapped entity.
if (i == cmd.pieces.length - 1 && cmd.endSnappedEntity)
{
var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (cmpEndSnappedObstruction)
constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
}
var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
if (pieceEntityId)
{
// wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
piece.ent = pieceEntityId;
// if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
if (piece.template == cmd.wallSet.templates.tower)
{
var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
var newTowerControlGroup = pieceEntityId;
if (i > 0)
{
//warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup);
var cmpPreviousObstruction = Engine.QueryInterface(cmd.pieces[i-1].ent, IID_Obstruction);
// TODO: ensure that cmpPreviousObstruction exists
// TODO: ensure that the previous obstruction does not yet have a secondary control group set
cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
}
// TODO: ensure that cmpTowerObstruction exists
cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
lastTowerIndex = i;
lastTowerControlGroup = newTowerControlGroup;
}
}
else
{
// failed to build wall piece, abort
i = j + 1; // compensate for the -1 subtracted by lastBuiltPieceIndex below
break;
}
}
var lastBuiltPieceIndex = i - 1;
var wallComplete = (lastBuiltPieceIndex == cmd.pieces.length - 1);
// At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
// Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
// as their secondary control groups.
lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// only start off with the ending side's snapped tower's control group if we were able to build the entire wall
if (cmd.endSnappedEntity && wallComplete)
{
var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (!cmpSnappedEndObstruction)
{
error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
}
for (var j = lastBuiltPieceIndex; j >= 0; --j)
{
var piece = cmd.pieces[j];
if (!piece.ent)
{
error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
continue;
}
var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
if (!cmpPieceObstruction)
{
error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
continue;
}
if (piece.template == cmd.wallSet.templates.tower)
{
// encountered a tower entity, update the last tower control group
lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
}
else
{
// Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
// Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
// dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
if (existingSecondaryControlGroup == INVALID_ENTITY)
{
if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
{
cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
}
}
else if (existingSecondaryControlGroup != lastTowerControlGroup)
{
error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
break;
}
}
}
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
var formation = ExtractFormations(ents);
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents, player, formationTemplate)
{
// If an individual was selected, remove it from any formation
// and command it individually
if (ents.length == 1)
{
// Skip unit if it has no UnitAI
var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
RemoveFromFormation(ents);
return [ cmpUnitAI ];
}
// Separate out the units that don't support the chosen formation
var formedEnts = [];
var nonformedUnitAIs = [];
for each (var ent in ents)
{
// Skip units with no UnitAI
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
if (cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "formations/line_closed"))
formedEnts.push(ent);
else
nonformedUnitAIs.push(cmpUnitAI);
}
if (formedEnts.length == 0)
{
// No units support the foundation - return all the others
return nonformedUnitAIs;
}
// Find what formations the formationable selected entities are currently in
var formation = ExtractFormations(formedEnts);
var formationUnitAIs = [];
if (formation.ids.length == 1)
{
// Selected units either belong to this formation or have no formation
// Check that all its members are selected
var fid = formation.ids[0];
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length
&& cmpFormation.GetMemberCount() == formation.entities.length)
{
cmpFormation.DeleteTwinFormations();
// The whole formation was selected, so reuse its controller for this command
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
cmpFormation.LoadFormation(formationTemplate);
}
}
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller
// Remove selected units from their current formation
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
var formationSeparation = 60;
var clusters = ClusterEntities(formation.entities, formationSeparation);
var formationEnts = [];
for each (var cluster in clusters)
{
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
// get the most recently used formation, or default to line closed
var lastFormationTemplate = undefined;
for each (var ent in cluster)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
var template = cmpUnitAI.GetLastFormationTemplate();
if (lastFormationTemplate === undefined)
{
lastFormationTemplate = template;
}
else if (lastFormationTemplate != template)
{
lastFormationTemplate = undefined;
break;
}
}
}
if (lastFormationTemplate && CanMoveEntsIntoFormation(cluster, lastFormationTemplate))
formationTemplate = lastFormationTemplate;
else
formationTemplate = "formations/line_closed";
}
// Create the new controller
var formationEnt = Engine.AddEntity(formationTemplate);
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
for each (var ent in formationEnts)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
}
}
return nonformedUnitAIs.concat(formationUnitAIs);
}
/**
* Group a list of entities in clusters via single-links
*/
function ClusterEntities(ents, separationDistance)
{
var clusters = [];
if (!ents.length)
return clusters;
var distSq = separationDistance * separationDistance;
var positions = [];
// triangular matrix with the (squared) distances between the different clusters
// the other half is not initialised
var matrix = [];
for (var i = 0; i < ents.length; i++)
{
matrix[i] = [];
clusters.push([ents[i]]);
var cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
if (!cmpPosition)
{
error("Asked to cluster entities without position: "+ents[i]);
return clusters;
}
positions.push(cmpPosition.GetPosition2D());
for (var j = 0; j < i; j++)
matrix[i][j] = positions[i].distanceToSquared(positions[j]);
}
while (clusters.length > 1)
{
// search two clusters that are closer than the required distance
var smallDist = Infinity;
var closeClusters = undefined;
for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i)
for (var j = i - 1; j >= 0 && !closeClusters; --j)
if (matrix[i][j] < distSq)
closeClusters = [i,j];
// if no more close clusters found, just return all found clusters so far
if (!closeClusters)
return clusters;
// make a new cluster with the entities from the two found clusters
var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
// calculate the minimum distance between the new cluster and all other remaining
// clusters by taking the minimum of the two distances.
var distances = [];
for (var i = 0; i < clusters.length; i++)
{
if (i == closeClusters[1] || i == closeClusters[0])
continue;
var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]];
var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]];
distances.push(Math.min(dist1, dist2));
}
// remove the rows and columns in the matrix for the merged clusters,
// and the clusters themselves from the cluster list
clusters.splice(closeClusters[0],1);
clusters.splice(closeClusters[1],1);
matrix.splice(closeClusters[0],1);
matrix.splice(closeClusters[1],1);
for (var i = 0; i < matrix.length; i++)
{
if (matrix[i].length > closeClusters[0])
matrix[i].splice(closeClusters[0],1);
if (matrix[i].length > closeClusters[1])
matrix[i].splice(closeClusters[1],1);
}
// add a new row of distances to the matrix and the new cluster
clusters.push(newCluster);
matrix.push(distances);
}
return clusters;
}
function GetFormationRequirements(formationTemplate)
{
var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempManager.GetTemplate(formationTemplate);
if (!template.Formation)
return false;
return {"minCount": +template.Formation.RequiredMemberCount};
}
function CanMoveEntsIntoFormation(ents, formationTemplate)
{
// TODO: should check the player's civ is allowed to use this formation
// See simulation/components/Player.js GetFormations() for a list of all allowed formations
var requirements = GetFormationRequirements(formationTemplate);
if (!requirements)
return false;
var count = 0;
var reqClasses = requirements.classesRequired || [];
for each (var ent in ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate))
continue;
count++;
}
return count >= requirements.minCount;
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player
* or control all units is activated, else false
*/
function CanControlUnit(entity, player, controlAll)
{
return (IsOwnedByPlayer(player, entity) || controlAll);
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player
* or the entity is owned by an mutualAlly
* or control all units is activated, else false
*/
function CanControlUnitOrIsAlly(entity, player, controlAll)
{
return (IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll);
}
/**
* Filter entities which the player can control
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(function(ent) { return CanControlUnit(ent, player, controlAll);} );
}
/**
* Filter entities which the player can control or are mutualAlly
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
return entities.filter(function(ent) { return CanControlUnitOrIsAlly(ent, player, controlAll);} );
}
/**
* Try to transform a wall to a gate
*/
function TryTransformWallToGate(ent, cmpPlayer, template)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
return;
// Check if this is a valid long wall segment
if (!cmpIdentity.HasClass("LongWall"))
{
if (g_DebugCommands)
warn("Invalid command: invalid wall conversion to gate for player "+player+": "+uneval(cmd));
return;
}
var civ = cmpIdentity.GetCiv();
var gate = Engine.AddEntity(template);
var cmpCost = Engine.QueryInterface(gate, IID_Cost);
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
if (g_DebugCommands)
warn("Invalid command: convert gate cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(gate);
return;
}
ReplaceBuildingWith(ent, gate);
}
/**
* Unconditionally replace a building with another one
*/
function ReplaceBuildingWith(ent, building)
{
// Move the building to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position);
var pos = cmpPosition.GetPosition2D();
cmpBuildingPosition.JumpTo(pos.x, pos.y);
var rot = cmpPosition.GetRotation();
cmpBuildingPosition.SetYRotation(rot.y);
cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
// Copy ownership
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
// Copy control groups
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
// Copy health level from the old entity to the new
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
var healthFraction = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
var buildingHitpoints = Math.round(cmpBuildingHealth.GetMaxHitpoints() * healthFraction);
cmpBuildingHealth.SetHitpoints(buildingHitpoints);
PlaySound("constructed", building);
Engine.PostMessage(ent, MT_ConstructionFinished,
{ "entity": ent, "newentity": building });
Engine.BroadcastMessage(MT_EntityRenamed, { entity: ent, newentity: building });
Engine.DestroyEntity(ent);
}
Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Engine.RegisterGlobal("commands", commands);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia.xml (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia.xml (revision 15612)
@@ -1,32 +1,33 @@
+ gaiaGaiatruetruetruetruetruefalsefalse2.00.3335.00truefalsefalsetruefalse
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 15611)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 15612)
@@ -1,114 +1,115 @@
11111100Ranged Infantrylandown001000000.03.09.8
+ corpse0truetrueStructureStructure ConquestCriticalstructuretruetruetruetruetruefalsefalsespecial/rallypointart/textures/misc/rallypoint_line.pngart/textures/misc/rallypoint_line_mask.png0.2squarerounddefaultdefaultoutline_border.pngoutline_border_mask.png0.4interface/complete/building/complete_universal.xmlattack/destruction/building_collapse_large.xmlinterface/alarm/alarm_attackplayer.xmlattack/weapon/arrowfly.xmlattack/impact/arrow_metal.xml6.00.612.0540truefalsefalsetruefalse
Index: ps/trunk/source/ps/TemplateLoader.cpp
===================================================================
--- ps/trunk/source/ps/TemplateLoader.cpp (revision 15611)
+++ ps/trunk/source/ps/TemplateLoader.cpp (revision 15612)
@@ -1,473 +1,523 @@
/* Copyright (C) 2014 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "TemplateLoader.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
#include "lib/utf8.h"
static const wchar_t TEMPLATE_ROOT[] = L"simulation/templates/";
static const wchar_t ACTOR_ROOT[] = L"art/actors/";
static CParamNode NULL_NODE(false);
bool CTemplateLoader::LoadTemplateFile(const std::string& templateName, int depth)
{
// If this file was already loaded, we don't need to do anything
if (m_TemplateFileData.find(templateName) != m_TemplateFileData.end())
return true;
// Handle infinite loops more gracefully than running out of stack space and crashing
if (depth > 100)
{
LOGERROR(L"Probable infinite inheritance loop in entity template '%hs'", templateName.c_str());
return false;
}
// Handle special case "actor|foo"
if (templateName.find("actor|") == 0)
{
ConstructTemplateActor(templateName.substr(6), m_TemplateFileData[templateName]);
return true;
}
// Handle special case "preview|foo"
if (templateName.find("preview|") == 0)
{
// Load the base entity template, if it wasn't already loaded
std::string baseName = templateName.substr(8);
if (!LoadTemplateFile(baseName, depth+1))
{
LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str());
return false;
}
// Copy a subset to the requested template
CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName], false);
return true;
}
// Handle special case "corpse|foo"
if (templateName.find("corpse|") == 0)
{
// Load the base entity template, if it wasn't already loaded
std::string baseName = templateName.substr(7);
if (!LoadTemplateFile(baseName, depth+1))
{
LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str());
return false;
}
// Copy a subset to the requested template
CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName], true);
return true;
}
+ // Handle special case "mirage|foo"
+ if (templateName.find("mirage|") == 0)
+ {
+ // Load the base entity template, if it wasn't already loaded
+ std::string baseName = templateName.substr(7);
+ if (!LoadTemplateFile(baseName, depth+1))
+ {
+ LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str());
+ return false;
+ }
+ // Copy a subset to the requested template
+ CopyMirageSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
+ return true;
+ }
+
// Handle special case "foundation|foo"
if (templateName.find("foundation|") == 0)
{
// Load the base entity template, if it wasn't already loaded
std::string baseName = templateName.substr(11);
if (!LoadTemplateFile(baseName, depth+1))
{
LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str());
return false;
}
// Copy a subset to the requested template
CopyFoundationSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
return true;
}
// Handle special case "construction|foo"
if (templateName.find("construction|") == 0)
{
// Load the base entity template, if it wasn't already loaded
std::string baseName = templateName.substr(13);
if (!LoadTemplateFile(baseName, depth+1))
{
LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str());
return false;
}
// Copy a subset to the requested template
CopyConstructionSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
return true;
}
// Handle special case "resource|foo"
if (templateName.find("resource|") == 0)
{
// Load the base entity template, if it wasn't already loaded
std::string baseName = templateName.substr(9);
if (!LoadTemplateFile(baseName, depth+1))
{
LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str());
return false;
}
// Copy a subset to the requested template
CopyResourceSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
return true;
}
// Normal case: templateName is an XML file:
VfsPath path = VfsPath(TEMPLATE_ROOT) / wstring_from_utf8(templateName + ".xml");
CXeromyces xero;
PSRETURN ok = xero.Load(g_VFS, path);
if (ok != PSRETURN_OK)
return false; // (Xeromyces already logged an error with the full filename)
int attr_parent = xero.GetAttributeID("parent");
CStr parentName = xero.GetRoot().GetAttributes().GetNamedItem(attr_parent);
if (!parentName.empty())
{
// To prevent needless complexity in template design, we don't allow |-separated strings as parents
if (parentName.find('|') != parentName.npos)
{
LOGERROR(L"Invalid parent '%hs' in entity template '%hs'", parentName.c_str(), templateName.c_str());
return false;
}
// Ensure the parent is loaded
if (!LoadTemplateFile(parentName, depth+1))
{
LOGERROR(L"Failed to load parent '%hs' of entity template '%hs'", parentName.c_str(), templateName.c_str());
return false;
}
CParamNode& parentData = m_TemplateFileData[parentName];
// Initialise this template with its parent
m_TemplateFileData[templateName] = parentData;
}
// Load the new file into the template data (overriding parent values)
CParamNode::LoadXML(m_TemplateFileData[templateName], xero, wstring_from_utf8(templateName).c_str());
return true;
}
static Status AddToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData)
{
std::vector& templates = *(std::vector*)cbData;
// Strip the .xml extension
VfsPath pathstem = pathname.ChangeExtension(L"");
// Strip the root from the path
std::wstring name = pathstem.string().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1);
// We want to ignore template_*.xml templates, since they should never be built in the editor
if (name.substr(0, 9) == L"template_")
return INFO::OK;
templates.push_back(std::string(name.begin(), name.end()));
return INFO::OK;
}
static Status AddActorToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData)
{
std::vector& templates = *(std::vector*)cbData;
// Strip the root from the path
std::wstring name = pathname.string().substr(ARRAY_SIZE(ACTOR_ROOT)-1);
templates.push_back("actor|" + std::string(name.begin(), name.end()));
return INFO::OK;
}
std::vector CTemplateLoader::FindPlaceableTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType, ScriptInterface& scriptInterface)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
std::vector templates;
Status ok;
VfsPath templatePath;
if (templatesType == SIMULATION_TEMPLATES || templatesType == ALL_TEMPLATES)
{
JS::RootedValue placeablesFilter(cx);
scriptInterface.ReadJSONFile("simulation/data/placeablesFilter.json", &placeablesFilter);
std::vector folders;
if (scriptInterface.GetProperty(placeablesFilter, "templates", folders))
{
templatePath = VfsPath(TEMPLATE_ROOT) / path;
//I have every object inside, just run for each
for (std::vector::iterator iterator = folders.begin(); iterator != folders.end();++iterator)
{
JS::RootedValue val(cx, (*iterator).get());
std::string directoryPath;
std::wstring fileFilter;
scriptInterface.GetProperty(val, "directory", directoryPath);
scriptInterface.GetProperty(val, "file", fileFilter);
VfsPaths filenames;
if (vfs::GetPathnames(g_VFS, templatePath / (directoryPath + "/"), fileFilter.c_str(), filenames) != INFO::OK)
continue;
for (VfsPaths::iterator it = filenames.begin(); it != filenames.end(); ++it)
{
VfsPath filename = *it;
// Strip the .xml extension
VfsPath pathstem = filename.ChangeExtension(L"");
// Strip the root from the path
std::wstring name = pathstem.string().substr(ARRAY_SIZE(TEMPLATE_ROOT) - 1);
templates.push_back(std::string(name.begin(), name.end()));
}
}
}
}
if (templatesType == ACTOR_TEMPLATES || templatesType == ALL_TEMPLATES)
{
templatePath = VfsPath(ACTOR_ROOT) / path;
if (includeSubdirectories)
ok = vfs::ForEachFile(g_VFS, templatePath, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", vfs::DIR_RECURSIVE);
else
ok = vfs::ForEachFile(g_VFS, templatePath, AddActorToTemplates, (uintptr_t)&templates, L"*.xml");
WARN_IF_ERR(ok);
}
if (templatesType != SIMULATION_TEMPLATES && templatesType != ACTOR_TEMPLATES && templatesType != ALL_TEMPLATES)
LOGERROR(L"Undefined template type (valid: all, simulation, actor)");
return templates;
}
std::vector CTemplateLoader::FindTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType)
{
std::vector templates;
Status ok;
VfsPath templatePath;
if (templatesType == SIMULATION_TEMPLATES || templatesType == ALL_TEMPLATES)
{
templatePath = VfsPath(TEMPLATE_ROOT) / path;
if (includeSubdirectories)
ok = vfs::ForEachFile(g_VFS, templatePath, AddToTemplates, (uintptr_t)&templates, L"*.xml", vfs::DIR_RECURSIVE);
else
ok = vfs::ForEachFile(g_VFS, templatePath, AddToTemplates, (uintptr_t)&templates, L"*.xml");
WARN_IF_ERR(ok);
}
if (templatesType == ACTOR_TEMPLATES || templatesType == ALL_TEMPLATES)
{
templatePath = VfsPath(ACTOR_ROOT) / path;
if (includeSubdirectories)
ok = vfs::ForEachFile(g_VFS, templatePath, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", vfs::DIR_RECURSIVE);
else
ok = vfs::ForEachFile(g_VFS, templatePath, AddActorToTemplates, (uintptr_t)&templates, L"*.xml");
WARN_IF_ERR(ok);
}
if (templatesType != SIMULATION_TEMPLATES && templatesType != ACTOR_TEMPLATES && templatesType != ALL_TEMPLATES)
LOGERROR(L"Undefined template type (valid: all, simulation, actor)");
return templates;
}
const CParamNode& CTemplateLoader::GetTemplateFileData(const std::string& templateName)
{
// Load the template if necessary
if (!LoadTemplateFile(templateName, 0))
{
LOGERROR(L"Failed to load entity template '%hs'", templateName.c_str());
return NULL_NODE;
}
return m_TemplateFileData[templateName];
}
void CTemplateLoader::ConstructTemplateActor(const std::string& actorName, CParamNode& out)
{
// Load the base actor template if necessary
const char* templateName = "special/actor";
if (!LoadTemplateFile(templateName, 0))
{
LOGERROR(L"Failed to load entity template '%hs'", templateName);
return;
}
// Copy the actor template
out = m_TemplateFileData[templateName];
// Initialise the actor's name and make it an Atlas selectable entity.
std::wstring actorNameW = wstring_from_utf8(actorName);
std::string name = utf8_from_wstring(CParamNode::EscapeXMLString(actorNameW));
std::string xml = ""
"" + name + ""
// arbitrary-sized Footprint definition to make actors' selection outlines show up in Atlas
"1.0"
""
""
"actor.pngactor_mask.png"
""
"";
CParamNode::LoadXMLString(out, xml.c_str(), actorNameW.c_str());
}
void CTemplateLoader::CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse)
{
// We only want to include components which are necessary (for the visual previewing of an entity)
// and safe (i.e. won't do anything that affects the synchronised simulation state), so additions
// to this list should be carefully considered
std::set permittedComponentTypes;
permittedComponentTypes.insert("Identity");
permittedComponentTypes.insert("Ownership");
permittedComponentTypes.insert("Position");
permittedComponentTypes.insert("VisualActor");
permittedComponentTypes.insert("Footprint");
permittedComponentTypes.insert("Obstruction");
permittedComponentTypes.insert("Decay");
permittedComponentTypes.insert("BuildRestrictions");
// Need these for the Actor Viewer:
permittedComponentTypes.insert("Attack");
permittedComponentTypes.insert("UnitMotion");
permittedComponentTypes.insert("Sound");
// (This set could be initialised once and reused, but it's not worth the effort)
CParamNode::LoadXMLString(out, "");
out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
// Disable the Obstruction component (if there is one) so it doesn't affect pathfinding
// (but can still be used for testing this entity for collisions against others)
if (out.GetChild("Entity").GetChild("Obstruction").IsOk())
CParamNode::LoadXMLString(out, "false");
if (!corpse)
{
// Previews should not cast shadows
if (out.GetChild("Entity").GetChild("VisualActor").IsOk())
CParamNode::LoadXMLString(out, "");
// Previews should always be visible in fog-of-war/etc
CParamNode::LoadXMLString(out, "0falsetrue");
}
if (corpse)
{
// Corpses should include decay components and un-inactivate them
if (out.GetChild("Entity").GetChild("Decay").IsOk())
CParamNode::LoadXMLString(out, "");
// Corpses shouldn't display silhouettes (especially since they're often half underground)
if (out.GetChild("Entity").GetChild("VisualActor").IsOk())
CParamNode::LoadXMLString(out, "false");
// Corpses should remain visible in fog-of-war
CParamNode::LoadXMLString(out, "0truefalse");
}
}
+void CTemplateLoader::CopyMirageSubset(CParamNode& out, const CParamNode& in)
+{
+ // Currently used for mirage entities replacing real ones in fog-of-war
+
+ std::set permittedComponentTypes;
+ permittedComponentTypes.insert("Footprint");
+ permittedComponentTypes.insert("Minimap");
+ permittedComponentTypes.insert("Ownership");
+ permittedComponentTypes.insert("Position");
+ permittedComponentTypes.insert("Selectable");
+ permittedComponentTypes.insert("VisualActor");
+
+ CParamNode::LoadXMLString(out, "");
+ out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
+
+ // Select a subset of identity data. We don't want to have, for example, a CC mirage
+ // that has also the CC class and then prevents construction of other CCs
+ std::set identitySubset;
+ identitySubset.insert("Civ");
+ identitySubset.insert("GenericName");
+ identitySubset.insert("SpecificName");
+ identitySubset.insert("Tooltip");
+ identitySubset.insert("History");
+ identitySubset.insert("Icon");
+ CParamNode identity;
+ CParamNode::LoadXMLString(identity, "");
+ identity.CopyFilteredChildrenOfChild(in.GetChild("Entity"), "Identity", identitySubset);
+ CParamNode::LoadXMLString(out, (""+utf8_from_wstring(identity.ToXML())+"").c_str());
+
+ // Set the entity as mirage entity
+ CParamNode::LoadXMLString(out, "");
+ CParamNode::LoadXMLString(out, "0truefalse");
+}
+
void CTemplateLoader::CopyFoundationSubset(CParamNode& out, const CParamNode& in)
{
// TODO: this is all kind of yucky and hard-coded; it'd be nice to have a more generic
// extensible scriptable way to define these subsets
std::set permittedComponentTypes;
permittedComponentTypes.insert("Ownership");
permittedComponentTypes.insert("Position");
permittedComponentTypes.insert("VisualActor");
permittedComponentTypes.insert("Identity");
permittedComponentTypes.insert("BuildRestrictions");
permittedComponentTypes.insert("Obstruction");
permittedComponentTypes.insert("Selectable");
permittedComponentTypes.insert("Footprint");
+ permittedComponentTypes.insert("Fogging");
permittedComponentTypes.insert("Armour");
permittedComponentTypes.insert("Health");
permittedComponentTypes.insert("StatusBars");
permittedComponentTypes.insert("OverlayRenderer");
permittedComponentTypes.insert("Decay");
permittedComponentTypes.insert("Cost");
permittedComponentTypes.insert("Sound");
permittedComponentTypes.insert("Vision");
permittedComponentTypes.insert("AIProxy");
permittedComponentTypes.insert("RallyPoint");
permittedComponentTypes.insert("RallyPointRenderer");
CParamNode::LoadXMLString(out, "");
out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
// Switch the actor to foundation mode
CParamNode::LoadXMLString(out, "");
// Add the Foundation component, to deal with the construction process
CParamNode::LoadXMLString(out, "");
// Initialise health to 1
CParamNode::LoadXMLString(out, "1");
// Foundations shouldn't initially block unit movement
if (out.GetChild("Entity").GetChild("Obstruction").IsOk())
CParamNode::LoadXMLString(out, "truetrue");
// Don't provide population bonuses yet (but still do take up population cost)
if (out.GetChild("Entity").GetChild("Cost").IsOk())
CParamNode::LoadXMLString(out, "0");
// Foundations should be visible themselves in fog-of-war if their base template is,
// but shouldn't have any vision range
if (out.GetChild("Entity").GetChild("Vision").IsOk())
CParamNode::LoadXMLString(out, "0");
}
void CTemplateLoader::CopyConstructionSubset(CParamNode& out, const CParamNode& in)
{
// Currently used for buildings rising during construction
// Mostly serves to filter out components like Vision, UnitAI, etc.
std::set permittedComponentTypes;
permittedComponentTypes.insert("Footprint");
permittedComponentTypes.insert("Ownership");
permittedComponentTypes.insert("Position");
permittedComponentTypes.insert("VisualActor");
CParamNode::LoadXMLString(out, "");
out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
}
void CTemplateLoader::CopyResourceSubset(CParamNode& out, const CParamNode& in)
{
// Currently used for animals which die and leave a gatherable corpse.
// Mostly serves to filter out components like Vision, UnitAI, etc.
std::set permittedComponentTypes;
permittedComponentTypes.insert("Ownership");
permittedComponentTypes.insert("Position");
permittedComponentTypes.insert("VisualActor");
permittedComponentTypes.insert("Identity");
permittedComponentTypes.insert("Obstruction");
permittedComponentTypes.insert("Minimap");
permittedComponentTypes.insert("ResourceSupply");
permittedComponentTypes.insert("Selectable");
permittedComponentTypes.insert("Footprint");
permittedComponentTypes.insert("StatusBars");
permittedComponentTypes.insert("OverlayRenderer");
permittedComponentTypes.insert("Sound");
permittedComponentTypes.insert("AIProxy");
CParamNode::LoadXMLString(out, "");
out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
}
Index: ps/trunk/source/ps/TemplateLoader.h
===================================================================
--- ps/trunk/source/ps/TemplateLoader.h (revision 15611)
+++ ps/trunk/source/ps/TemplateLoader.h (revision 15612)
@@ -1,108 +1,114 @@
/* Copyright (C) 2014 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_TEMPLATELOADER
#define INCLUDED_TEMPLATELOADER
#include "simulation2/system/ParamNode.h"
#include "scriptinterface/ScriptInterface.h"
enum ETemplatesType
{
ALL_TEMPLATES,
ACTOR_TEMPLATES,
SIMULATION_TEMPLATES
};
/**
* Template loader: Handles the loading of entity template files for:
* - the initialisation and deserialization of entity components in the
* simulation (CmpTemplateManager).
* - access to actor templates, obstruction data, etc. in RMS/RMGEN
*
* Template names are intentionally restricted to ASCII strings for storage/serialization
* efficiency (we have a lot of strings so this is significant);
* they correspond to filenames so they shouldn't contain non-ASCII anyway.
*/
class CTemplateLoader
{
public:
CTemplateLoader()
{
}
/**
* Provides the file data for requested template.
*/
const CParamNode &GetTemplateFileData(const std::string& templateName);
/**
* Returns a list of strings that could be validly passed as @c templateName to LoadTemplateFile.
* (This includes "actor|foo" etc names).
*/
std::vector FindTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType);
std::vector FindPlaceableTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType, ScriptInterface& scriptInterface);
private:
/**
* (Re)loads the given template, regardless of whether it exists already,
* and saves into m_TemplateFileData. Also loads any parents that are not yet
* loaded. Returns false on error.
* @param templateName XML filename to load (not a |-separated string)
*/
bool LoadTemplateFile(const std::string& templateName, int depth);
/**
* Constructs a standard static-decorative-object template for the given actor
*/
void ConstructTemplateActor(const std::string& actorName, CParamNode& out);
/**
* Copy the non-interactive components of an entity template (position, actor, etc) into
* a new entity template
*/
void CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse);
/**
+ * Copy the components of an entity template necessary for a fogged "mirage"
+ * entity (position, actor) into a new entity template
+ */
+ void CopyMirageSubset(CParamNode& out, const CParamNode& in);
+
+ /**
* Copy the components of an entity template necessary for a construction foundation
* (position, actor, armour, health, etc) into a new entity template
*/
void CopyFoundationSubset(CParamNode& out, const CParamNode& in);
/**
* Copy the components of an entity template necessary for a non-foundation construction entity
* into a new entity template
*/
void CopyConstructionSubset(CParamNode& out, const CParamNode& in);
/**
* Copy the components of an entity template necessary for a gatherable resource
* into a new entity template
*/
void CopyResourceSubset(CParamNode& out, const CParamNode& in);
/**
* Map from template name (XML filename or special |-separated string) to the most recently
* loaded non-broken template data. This includes files that will fail schema validation.
* (Failed loads won't remove existing entries under the same name, so we behave more nicely
* when hotloading broken files)
*/
std::map m_TemplateFileData;
};
#endif // INCLUDED_TEMPLATELOADER
Index: ps/trunk/source/simulation2/TypeList.h
===================================================================
--- ps/trunk/source/simulation2/TypeList.h (revision 15611)
+++ ps/trunk/source/simulation2/TypeList.h (revision 15612)
@@ -1,180 +1,186 @@
/* Copyright (C) 2014 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 .
*/
// MESSAGE: message types
// INTERFACE: component interface types
// COMPONENT: component types
// Components intended only for use in test cases:
// (The tests rely on the enum IDs, so don't change the order of these)
INTERFACE(Test1)
COMPONENT(Test1A)
COMPONENT(Test1B)
COMPONENT(Test1Scripted)
INTERFACE(Test2)
COMPONENT(Test2A)
COMPONENT(Test2Scripted)
// Message types:
MESSAGE(TurnStart)
MESSAGE(Update)
MESSAGE(Update_MotionFormation)
MESSAGE(Update_MotionUnit)
MESSAGE(Update_Final)
MESSAGE(Interpolate) // non-deterministic (use with caution)
MESSAGE(RenderSubmit) // non-deterministic (use with caution)
MESSAGE(ProgressiveLoad) // non-deterministic (use with caution)
MESSAGE(Deserialized) // non-deterministic (use with caution)
MESSAGE(Create)
MESSAGE(Destroy)
MESSAGE(OwnershipChanged)
MESSAGE(PositionChanged)
MESSAGE(InterpolatedPositionChanged)
MESSAGE(TerritoryPositionChanged)
MESSAGE(MotionChanged)
MESSAGE(RangeUpdate)
MESSAGE(TerrainChanged)
MESSAGE(VisibilityChanged)
MESSAGE(WaterChanged)
MESSAGE(ObstructionMapShapeChanged)
MESSAGE(TerritoriesChanged)
MESSAGE(PathResult)
MESSAGE(ValueModification)
MESSAGE(TemplateModification)
MESSAGE(VisionRangeChanged)
MESSAGE(MinimapPing)
// TemplateManager must come before all other (non-test) components,
// so that it is the first to be (de)serialized
INTERFACE(TemplateManager)
COMPONENT(TemplateManager)
// Special component for script component types with no native interface
INTERFACE(UnknownScript)
COMPONENT(UnknownScript)
// In alphabetical order:
INTERFACE(AIInterface)
COMPONENT(AIInterfaceScripted)
INTERFACE(AIManager)
COMPONENT(AIManager)
INTERFACE(CommandQueue)
COMPONENT(CommandQueue)
INTERFACE(Decay)
COMPONENT(Decay)
+INTERFACE(Fogging)
+COMPONENT(FoggingScripted)
+
// Note: The VisualActor component relies on this component being initialized before itself, in order to support using
// an entity's footprint shape for the selection boxes. This dependency is not strictly necessary, but it does avoid
// some extra plumbing code to set up on-demand initialization. If you find yourself forced to break this dependency,
// see VisualActor's Init method for a description of how you can avoid it.
INTERFACE(Footprint)
COMPONENT(Footprint)
INTERFACE(GuiInterface)
COMPONENT(GuiInterfaceScripted)
INTERFACE(Identity)
COMPONENT(IdentityScripted)
INTERFACE(Minimap)
COMPONENT(Minimap)
+INTERFACE(Mirage)
+COMPONENT(MirageScripted)
+
INTERFACE(Motion)
COMPONENT(MotionBall)
COMPONENT(MotionScripted)
INTERFACE(Obstruction)
COMPONENT(Obstruction)
INTERFACE(ObstructionManager)
COMPONENT(ObstructionManager)
INTERFACE(OverlayRenderer)
COMPONENT(OverlayRenderer)
INTERFACE(Ownership)
COMPONENT(Ownership)
INTERFACE(ParticleManager)
COMPONENT(ParticleManager)
INTERFACE(Pathfinder)
COMPONENT(Pathfinder)
INTERFACE(Player)
COMPONENT(PlayerScripted)
INTERFACE(PlayerManager)
COMPONENT(PlayerManagerScripted)
INTERFACE(Position)
COMPONENT(Position) // must be before VisualActor
INTERFACE(ProjectileManager)
COMPONENT(ProjectileManager)
INTERFACE(RallyPointRenderer)
COMPONENT(RallyPointRenderer)
INTERFACE(RangeManager)
COMPONENT(RangeManager)
INTERFACE(Selectable)
COMPONENT(Selectable)
INTERFACE(Settlement)
COMPONENT(SettlementScripted)
INTERFACE(SoundManager)
COMPONENT(SoundManager)
INTERFACE(ValueModificationManager)
COMPONENT(ValueModificationManagerScripted)
INTERFACE(TechnologyTemplateManager)
COMPONENT(TechnologyTemplateManagerScripted)
INTERFACE(Terrain)
COMPONENT(Terrain)
INTERFACE(TerritoryInfluence)
COMPONENT(TerritoryInfluence)
INTERFACE(TerritoryManager)
COMPONENT(TerritoryManager)
INTERFACE(UnitMotion)
COMPONENT(UnitMotion) // must be after Obstruction
COMPONENT(UnitMotionScripted)
INTERFACE(UnitRenderer)
COMPONENT(UnitRenderer)
INTERFACE(Vision)
COMPONENT(Vision)
// Note: this component relies on the Footprint component being initialized before itself. See the comments above for
// the Footprint component to find out why.
INTERFACE(Visual)
COMPONENT(VisualActor) // must be after Ownership (dependency in Deserialize) and Vision (dependency in Init)
INTERFACE(WaterManager)
COMPONENT(WaterManager)
Index: ps/trunk/source/simulation2/components/CCmpRangeManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 15611)
+++ ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 15612)
@@ -1,1949 +1,1991 @@
/* Copyright (C) 2014 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpRangeManager.h"
#include "ICmpTerrain.h"
#include "simulation2/system/EntityMap.h"
#include "simulation2/MessageTypes.h"
+#include "simulation2/components/ICmpFogging.h"
+#include "simulation2/components/ICmpMirage.h"
+#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/components/ICmpVision.h"
#include "simulation2/components/ICmpWaterManager.h"
#include "simulation2/helpers/Render.h"
#include "simulation2/helpers/Spatial.h"
#include "graphics/Overlay.h"
#include "graphics/Terrain.h"
#include "lib/timer.h"
#include "ps/CLogger.h"
#include "ps/Overlay.h"
#include "ps/Profile.h"
#include "renderer/Scene.h"
#include "lib/ps_stl.h"
#define LOS_TILES_RATIO 8
#define DEBUG_RANGE_MANAGER_BOUNDS 0
/**
* Representation of a range query.
*/
struct Query
{
bool enabled;
bool parabolic;
CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it
entity_pos_t minRange;
entity_pos_t maxRange;
entity_pos_t elevationBonus;
u32 ownersMask;
i32 interface;
std::vector lastMatch;
u8 flagsMask;
};
/**
* Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players)
* into a 32-bit mask for quick set-membership tests.
*/
static inline u32 CalcOwnerMask(player_id_t owner)
{
if (owner >= -1 && owner < 31)
return 1 << (1+owner);
else
return 0; // owner was invalid
}
/**
* Returns LOS mask for given player.
*/
static inline u32 CalcPlayerLosMask(player_id_t player)
{
if (player > 0 && player <= 16)
return ICmpRangeManager::LOS_MASK << (2*(player-1));
return 0;
}
/**
* Returns shared LOS mask for given list of players.
*/
static u32 CalcSharedLosMask(std::vector players)
{
u32 playerMask = 0;
for (size_t i = 0; i < players.size(); i++)
playerMask |= CalcPlayerLosMask(players[i]);
return playerMask;
}
/**
* Checks whether v is in a parabolic range of (0,0,0)
* The highest point of the paraboloid is (0,range/2,0)
* and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid
*
* Avoids sqrting and overflowing.
*/
static bool InParabolicRange(CFixedVector3D v, fixed range)
{
i32 x = v.X.GetInternalValue(); // abs(x) <= 2^31
i32 z = v.Z.GetInternalValue();
u64 xx = (u64)FIXED_MUL_I64_I32_I32(x, x); // xx <= 2^62
u64 zz = (u64)FIXED_MUL_I64_I32_I32(z, z);
i64 d2 = (xx + zz) >> 1; // d2 <= 2^62 (no overflow)
i32 y = v.Y.GetInternalValue();
i32 c = range.GetInternalValue();
i32 c_2 = c >> 1;
i64 c2 = FIXED_MUL_I64_I32_I32(c_2 - y, c);
if (d2 <= c2)
return true;
return false;
}
struct EntityParabolicRangeOutline
{
entity_id_t source;
CFixedVector3D position;
entity_pos_t range;
std::vector outline;
};
static std::map ParabolicRangesOutlines;
/**
* Representation of an entity, with the data needed for queries.
*/
struct EntityData
{
EntityData() : visibilities(0), retainInFog(0), owner(-1), inWorld(0), flags(1) { }
entity_pos_t x, z;
entity_pos_t visionRange;
u32 visibilities; // 2-bit visibility, per player
u8 retainInFog; // boolean
i8 owner;
u8 inWorld; // boolean
u8 flags; // See GetEntityFlagMask
};
cassert(sizeof(EntityData) == 20);
/**
* Serialization helper template for Query
*/
struct SerializeQuery
{
template
void Common(S& serialize, const char* UNUSED(name), Query& value)
{
serialize.Bool("enabled", value.enabled);
serialize.Bool("parabolic",value.parabolic);
serialize.NumberFixed_Unbounded("min range", value.minRange);
serialize.NumberFixed_Unbounded("max range", value.maxRange);
serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus);
serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
serialize.NumberI32_Unbounded("interface", value.interface);
SerializeVector()(serialize, "last match", value.lastMatch);
serialize.NumberU8_Unbounded("flagsMask", value.flagsMask);
}
void operator()(ISerializer& serialize, const char* name, Query& value, const CSimContext& UNUSED(context))
{
Common(serialize, name, value);
uint32_t id = value.source.GetId();
serialize.NumberU32_Unbounded("source", id);
}
void operator()(IDeserializer& deserialize, const char* name, Query& value, const CSimContext& context)
{
Common(deserialize, name, value);
uint32_t id;
deserialize.NumberU32_Unbounded("source", id);
value.source = context.GetComponentManager().LookupEntityHandle(id, true);
// the referenced entity might not have been deserialized yet,
// so tell LookupEntityHandle to allocate the handle if necessary
}
};
/**
* Serialization helper template for EntityData
*/
struct SerializeEntityData
{
template
void operator()(S& serialize, const char* UNUSED(name), EntityData& value)
{
serialize.NumberFixed_Unbounded("x", value.x);
serialize.NumberFixed_Unbounded("z", value.z);
serialize.NumberFixed_Unbounded("vision", value.visionRange);
serialize.NumberU32_Unbounded("visibilities", value.visibilities);
serialize.NumberU8("retain in fog", value.retainInFog, 0, 1);
serialize.NumberI8_Unbounded("owner", value.owner);
serialize.NumberU8("in world", value.inWorld, 0, 1);
serialize.NumberU8_Unbounded("flags", value.flags);
}
};
/**
* Functor for sorting entities by distance from a source point.
* It must only be passed entities that are in 'entities'
* and are currently in the world.
*/
struct EntityDistanceOrdering
{
EntityDistanceOrdering(const EntityMap& entities, const CFixedVector2D& source) :
m_EntityData(entities), m_Source(source)
{
}
bool operator()(entity_id_t a, entity_id_t b)
{
const EntityData& da = m_EntityData.find(a)->second;
const EntityData& db = m_EntityData.find(b)->second;
CFixedVector2D vecA = CFixedVector2D(da.x, da.z) - m_Source;
CFixedVector2D vecB = CFixedVector2D(db.x, db.z) - m_Source;
return (vecA.CompareLength(vecB) < 0);
}
const EntityMap& m_EntityData;
CFixedVector2D m_Source;
private:
EntityDistanceOrdering& operator=(const EntityDistanceOrdering&);
};
/**
* Range manager implementation.
* Maintains a list of all entities (and their positions and owners), which is used for
* queries.
*
* LOS implementation is based on the model described in GPG2.
* (TODO: would be nice to make it cleverer, so e.g. mountains and walls
* can block vision)
*/
class CCmpRangeManager : public ICmpRangeManager
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeGloballyToMessageType(MT_Create);
componentManager.SubscribeGloballyToMessageType(MT_PositionChanged);
componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged);
componentManager.SubscribeGloballyToMessageType(MT_Destroy);
componentManager.SubscribeGloballyToMessageType(MT_VisionRangeChanged);
componentManager.SubscribeToMessageType(MT_Update);
componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays
}
DEFAULT_COMPONENT_ALLOCATOR(RangeManager)
bool m_DebugOverlayEnabled;
bool m_DebugOverlayDirty;
std::vector m_DebugOverlayLines;
// World bounds (entities are expected to be within this range)
entity_pos_t m_WorldX0;
entity_pos_t m_WorldZ0;
entity_pos_t m_WorldX1;
entity_pos_t m_WorldZ1;
// Range query state:
tag_t m_QueryNext; // next allocated id
std::map m_Queries;
EntityMap m_EntityData;
SpatialSubdivision m_Subdivision; // spatial index of m_EntityData
// LOS state:
static const player_id_t MAX_LOS_PLAYER_ID = 16;
std::vector m_LosRevealAll;
bool m_LosCircular;
i32 m_TerrainVerticesPerSide;
size_t m_TerritoriesDirtyID;
// Cache for visibility tracking (not serialized)
i32 m_LosTilesPerSide;
std::vector m_DirtyVisibility;
std::vector > m_LosTiles;
// List of entities that must be updated, regardless of the status of their tile
std::vector m_ModifiedEntities;
// Counts of units seeing vertex, per vertex, per player (starting with player 0).
// Use u16 to avoid overflows when we have very large (but not infeasibly large) numbers
// of units in a very small area.
// (Note we use vertexes, not tiles, to better match the renderer.)
// Lazily constructed when it's needed, to save memory in smaller games.
std::vector > m_LosPlayerCounts;
// 2-bit ELosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive)
std::vector m_LosState;
// Special static visibility data for the "reveal whole map" mode
// (TODO: this is usually a waste of memory)
std::vector m_LosStateRevealed;
// Shared LOS masks, one per player.
std::vector m_SharedLosMasks;
// Cache explored vertices per player (not serialized)
u32 m_TotalInworldVertices;
std::vector m_ExploredVertices;
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_QueryNext = 1;
m_DebugOverlayEnabled = false;
m_DebugOverlayDirty = true;
m_WorldX0 = m_WorldZ0 = m_WorldX1 = m_WorldZ1 = entity_pos_t::Zero();
// Initialise with bogus values (these will get replaced when
// SetBounds is called)
ResetSubdivisions(entity_pos_t::FromInt(1024), entity_pos_t::FromInt(1024));
// The whole map should be visible to Gaia by default, else e.g. animals
// will get confused when trying to run from enemies
m_LosRevealAll.resize(MAX_LOS_PLAYER_ID+2,false);
m_LosRevealAll[0] = true;
m_SharedLosMasks.resize(MAX_LOS_PLAYER_ID+2,0);
m_LosCircular = false;
m_TerrainVerticesPerSide = 0;
m_TerritoriesDirtyID = 0;
}
virtual void Deinit()
{
}
template
void SerializeCommon(S& serialize)
{
serialize.NumberFixed_Unbounded("world x0", m_WorldX0);
serialize.NumberFixed_Unbounded("world z0", m_WorldZ0);
serialize.NumberFixed_Unbounded("world x1", m_WorldX1);
serialize.NumberFixed_Unbounded("world z1", m_WorldZ1);
serialize.NumberU32_Unbounded("query next", m_QueryNext);
SerializeMap()(serialize, "queries", m_Queries, GetSimContext());
SerializeEntityMap()(serialize, "entity data", m_EntityData);
SerializeVector()(serialize, "los reveal all", m_LosRevealAll);
serialize.Bool("los circular", m_LosCircular);
serialize.NumberI32_Unbounded("terrain verts per side", m_TerrainVerticesPerSide);
SerializeVector()(serialize, "modified entities", m_ModifiedEntities);
// We don't serialize m_Subdivision, m_LosPlayerCounts or m_LosTiles
// since they can be recomputed from the entity data when deserializing;
// m_LosState must be serialized since it depends on the history of exploration
SerializeVector()(serialize, "los state", m_LosState);
SerializeVector()(serialize, "shared los masks", m_SharedLosMasks);
}
virtual void Serialize(ISerializer& serialize)
{
SerializeCommon(serialize);
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
SerializeCommon(deserialize);
// Reinitialise subdivisions and LOS data
ResetDerivedData(true);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Create:
{
const CMessageCreate& msgData = static_cast (msg);
entity_id_t ent = msgData.entity;
// Ignore local entities - we shouldn't let them influence anything
if (ENTITY_IS_LOCAL(ent))
break;
// Ignore non-positional entities
CmpPtr cmpPosition(GetSimContext(), ent);
if (!cmpPosition)
break;
// The newly-created entity will have owner -1 and position out-of-world
// (any initialisation of those values will happen later), so we can just
// use the default-constructed EntityData here
EntityData entdata;
// Store the LOS data, if any
CmpPtr cmpVision(GetSimContext(), ent);
if (cmpVision)
{
entdata.visionRange = cmpVision->GetRange();
entdata.retainInFog = (cmpVision->GetRetainInFog() ? 1 : 0);
}
// Remember this entity
m_EntityData.insert(ent, entdata);
break;
}
case MT_PositionChanged:
{
const CMessagePositionChanged& msgData = static_cast (msg);
entity_id_t ent = msgData.entity;
EntityMap::iterator it = m_EntityData.find(ent);
// Ignore if we're not already tracking this entity
if (it == m_EntityData.end())
break;
if (msgData.inWorld)
{
if (it->second.inWorld)
{
CFixedVector2D from(it->second.x, it->second.z);
CFixedVector2D to(msgData.x, msgData.z);
m_Subdivision.Move(ent, from, to);
LosMove(it->second.owner, it->second.visionRange, from, to);
i32 oldLosTile = PosToLosTilesHelper(it->second.x, it->second.z);
i32 newLosTile = PosToLosTilesHelper(msgData.x, msgData.z);
if (oldLosTile != newLosTile)
{
RemoveFromTile(oldLosTile, ent);
AddToTile(newLosTile, ent);
}
}
else
{
CFixedVector2D to(msgData.x, msgData.z);
m_Subdivision.Add(ent, to);
LosAdd(it->second.owner, it->second.visionRange, to);
AddToTile(PosToLosTilesHelper(msgData.x, msgData.z), ent);
}
it->second.inWorld = 1;
it->second.x = msgData.x;
it->second.z = msgData.z;
}
else
{
if (it->second.inWorld)
{
CFixedVector2D from(it->second.x, it->second.z);
m_Subdivision.Remove(ent, from);
LosRemove(it->second.owner, it->second.visionRange, from);
RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent);
}
it->second.inWorld = 0;
it->second.x = entity_pos_t::Zero();
it->second.z = entity_pos_t::Zero();
}
m_ModifiedEntities.push_back(ent);
break;
}
case MT_OwnershipChanged:
{
const CMessageOwnershipChanged& msgData = static_cast (msg);
entity_id_t ent = msgData.entity;
EntityMap::iterator it = m_EntityData.find(ent);
// Ignore if we're not already tracking this entity
if (it == m_EntityData.end())
break;
if (it->second.inWorld)
{
CFixedVector2D pos(it->second.x, it->second.z);
LosRemove(it->second.owner, it->second.visionRange, pos);
LosAdd(msgData.to, it->second.visionRange, pos);
}
ENSURE(-128 <= msgData.to && msgData.to <= 127);
it->second.owner = (i8)msgData.to;
break;
}
case MT_Destroy:
{
const CMessageDestroy& msgData = static_cast (msg);
entity_id_t ent = msgData.entity;
EntityMap::iterator it = m_EntityData.find(ent);
// Ignore if we're not already tracking this entity
if (it == m_EntityData.end())
break;
if (it->second.inWorld)
{
m_Subdivision.Remove(ent, CFixedVector2D(it->second.x, it->second.z));
RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent);
}
// This will be called after Ownership's OnDestroy, so ownership will be set
// to -1 already and we don't have to do a LosRemove here
ENSURE(it->second.owner == -1);
m_EntityData.erase(it);
break;
}
case MT_VisionRangeChanged:
{
const CMessageVisionRangeChanged& msgData = static_cast (msg);
entity_id_t ent = msgData.entity;
EntityMap::iterator it = m_EntityData.find(ent);
// Ignore if we're not already tracking this entity
if (it == m_EntityData.end())
break;
CmpPtr cmpVision(GetSimContext(), ent);
if (!cmpVision)
break;
entity_pos_t oldRange = it->second.visionRange;
entity_pos_t newRange = msgData.newRange;
// If the range changed and the entity's in-world, we need to manually adjust it
// but if it's not in-world, we only need to set the new vision range
CFixedVector2D pos(it->second.x, it->second.z);
if (it->second.inWorld)
LosRemove(it->second.owner, oldRange, pos);
it->second.visionRange = newRange;
if (it->second.inWorld)
LosAdd(it->second.owner, newRange, pos);
break;
}
case MT_Update:
{
m_DebugOverlayDirty = true;
- UpdateTerritoriesLos();
ExecuteActiveQueries();
UpdateVisibilityData();
break;
}
case MT_RenderSubmit:
{
const CMessageRenderSubmit& msgData = static_cast (msg);
RenderSubmit(msgData.collector);
break;
}
}
}
virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices)
{
m_WorldX0 = x0;
m_WorldZ0 = z0;
m_WorldX1 = x1;
m_WorldZ1 = z1;
m_TerrainVerticesPerSide = (i32)vertices;
ResetDerivedData(false);
}
virtual void Verify()
{
// Ignore if map not initialised yet
if (m_WorldX1.IsZero())
return;
// Check that calling ResetDerivedData (i.e. recomputing all the state from scratch)
// does not affect the incrementally-computed state
std::vector > oldPlayerCounts = m_LosPlayerCounts;
std::vector oldStateRevealed = m_LosStateRevealed;
SpatialSubdivision oldSubdivision = m_Subdivision;
std::vector > oldLosTiles = m_LosTiles;
ResetDerivedData(true);
if (oldPlayerCounts != m_LosPlayerCounts)
{
for (size_t i = 0; i < oldPlayerCounts.size(); ++i)
{
debug_printf(L"%d: ", (int)i);
for (size_t j = 0; j < oldPlayerCounts[i].size(); ++j)
debug_printf(L"%d ", oldPlayerCounts[i][j]);
debug_printf(L"\n");
}
for (size_t i = 0; i < m_LosPlayerCounts.size(); ++i)
{
debug_printf(L"%d: ", (int)i);
for (size_t j = 0; j < m_LosPlayerCounts[i].size(); ++j)
debug_printf(L"%d ", m_LosPlayerCounts[i][j]);
debug_printf(L"\n");
}
debug_warn(L"inconsistent player counts");
}
if (oldStateRevealed != m_LosStateRevealed)
debug_warn(L"inconsistent revealed");
if (oldSubdivision != m_Subdivision)
debug_warn(L"inconsistent subdivs");
if (oldLosTiles != m_LosTiles)
debug_warn(L"inconsistent los tiles");
}
SpatialSubdivision* GetSubdivision()
{
return & m_Subdivision;
}
// Reinitialise subdivisions and LOS data, based on entity data
void ResetDerivedData(bool skipLosState)
{
ENSURE(m_WorldX0.IsZero() && m_WorldZ0.IsZero()); // don't bother implementing non-zero offsets yet
ResetSubdivisions(m_WorldX1, m_WorldZ1);
m_LosTilesPerSide = (m_TerrainVerticesPerSide - 1)/LOS_TILES_RATIO;
m_LosPlayerCounts.clear();
m_LosPlayerCounts.resize(MAX_LOS_PLAYER_ID+1);
m_ExploredVertices.clear();
m_ExploredVertices.resize(MAX_LOS_PLAYER_ID+1, 0);
if (skipLosState)
{
// recalc current exploration stats.
for (i32 j = 0; j < m_TerrainVerticesPerSide; j++)
{
for (i32 i = 0; i < m_TerrainVerticesPerSide; i++)
{
if (!LosIsOffWorld(i, j))
{
for (u8 k = 1; k < MAX_LOS_PLAYER_ID+1; ++k)
m_ExploredVertices.at(k) += ((m_LosState[j*m_TerrainVerticesPerSide + i] & (LOS_EXPLORED << (2*(k-1)))) > 0);
}
}
}
}
else
{
m_LosState.clear();
m_LosState.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide);
}
m_LosStateRevealed.clear();
m_LosStateRevealed.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide);
m_DirtyVisibility.clear();
m_DirtyVisibility.resize(m_LosTilesPerSide*m_LosTilesPerSide, 1);
m_LosTiles.clear();
m_LosTiles.resize(m_LosTilesPerSide*m_LosTilesPerSide);
for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it)
{
if (it->second.inWorld)
{
LosAdd(it->second.owner, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z));
AddToTile(PosToLosTilesHelper(it->second.x, it->second.z), it->first);
}
}
m_TotalInworldVertices = 0;
for (ssize_t j = 0; j < m_TerrainVerticesPerSide; ++j)
for (ssize_t i = 0; i < m_TerrainVerticesPerSide; ++i)
{
if (LosIsOffWorld(i,j))
m_LosStateRevealed[i + j*m_TerrainVerticesPerSide] = 0;
else
{
m_LosStateRevealed[i + j*m_TerrainVerticesPerSide] = 0xFFFFFFFFu;
m_TotalInworldVertices++;
}
}
}
void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1)
{
// Use 8x8 tile subdivisions
// (TODO: find the optimal number instead of blindly guessing)
m_Subdivision.Reset(x1, z1, entity_pos_t::FromInt(8*TERRAIN_TILE_SIZE));
for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it)
{
if (it->second.inWorld)
m_Subdivision.Add(it->first, CFixedVector2D(it->second.x, it->second.z));
}
}
virtual tag_t CreateActiveQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange,
std::vector owners, int requiredInterface, u8 flags)
{
tag_t id = m_QueryNext++;
m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags);
return id;
}
virtual tag_t CreateActiveParabolicQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus,
std::vector owners, int requiredInterface, u8 flags)
{
tag_t id = m_QueryNext++;
m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, elevationBonus, owners, requiredInterface, flags);
return id;
}
virtual void DestroyActiveQuery(tag_t tag)
{
if (m_Queries.find(tag) == m_Queries.end())
{
LOGERROR(L"CCmpRangeManager: DestroyActiveQuery called with invalid tag %u", tag);
return;
}
m_Queries.erase(tag);
}
virtual void EnableActiveQuery(tag_t tag)
{
std::map::iterator it = m_Queries.find(tag);
if (it == m_Queries.end())
{
LOGERROR(L"CCmpRangeManager: EnableActiveQuery called with invalid tag %u", tag);
return;
}
Query& q = it->second;
q.enabled = true;
}
virtual void DisableActiveQuery(tag_t tag)
{
std::map::iterator it = m_Queries.find(tag);
if (it == m_Queries.end())
{
LOGERROR(L"CCmpRangeManager: DisableActiveQuery called with invalid tag %u", tag);
return;
}
Query& q = it->second;
q.enabled = false;
}
virtual std::vector ExecuteQueryAroundPos(CFixedVector2D pos,
entity_pos_t minRange, entity_pos_t maxRange,
std::vector owners, int requiredInterface)
{
Query q = ConstructQuery(INVALID_ENTITY, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"));
std::vector r;
PerformQuery(q, r, pos);
// Return the list sorted by distance from the entity
std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos));
return r;
}
virtual std::vector ExecuteQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange,
std::vector owners, int requiredInterface)
{
PROFILE("ExecuteQuery");
Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"));
std::vector r;
CmpPtr cmpSourcePosition(q.source);
if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld())
{
// If the source doesn't have a position, then the result is just the empty list
return r;
}
CFixedVector2D pos = cmpSourcePosition->GetPosition2D();
PerformQuery(q, r, pos);
// Return the list sorted by distance from the entity
std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos));
return r;
}
virtual std::vector ResetActiveQuery(tag_t tag)
{
PROFILE("ResetActiveQuery");
std::vector r;
std::map::iterator it = m_Queries.find(tag);
if (it == m_Queries.end())
{
LOGERROR(L"CCmpRangeManager: ResetActiveQuery called with invalid tag %u", tag);
return r;
}
Query& q = it->second;
q.enabled = true;
CmpPtr cmpSourcePosition(q.source);
if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld())
{
// If the source doesn't have a position, then the result is just the empty list
q.lastMatch = r;
return r;
}
CFixedVector2D pos = cmpSourcePosition->GetPosition2D();
PerformQuery(q, r, pos);
q.lastMatch = r;
// Return the list sorted by distance from the entity
std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos));
return r;
}
virtual std::vector GetEntitiesByPlayer(player_id_t player)
{
std::vector entities;
u32 ownerMask = CalcOwnerMask(player);
for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it)
{
// Check owner and add to list if it matches
if (CalcOwnerMask(it->second.owner) & ownerMask)
entities.push_back(it->first);
}
return entities;
}
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlayEnabled = enabled;
m_DebugOverlayDirty = true;
if (!enabled)
m_DebugOverlayLines.clear();
}
/**
* Update all currently-enabled active queries.
*/
void ExecuteActiveQueries()
{
PROFILE3("ExecuteActiveQueries");
// Store a queue of all messages before sending any, so we can assume
// no entities will move until we've finished checking all the ranges
std::vector > messages;
std::vector results;
std::vector added;
std::vector removed;
for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it)
{
Query& query = it->second;
if (!query.enabled)
continue;
CmpPtr cmpSourcePosition(query.source);
if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld())
continue;
results.clear();
results.reserve(query.lastMatch.size());
CFixedVector2D pos = cmpSourcePosition->GetPosition2D();
PerformQuery(query, results, pos);
// Compute the changes vs the last match
added.clear();
removed.clear();
// Return the 'added' list sorted by distance from the entity
// (Don't bother sorting 'removed' because they might not even have positions or exist any more)
std::set_difference(results.begin(), results.end(), query.lastMatch.begin(), query.lastMatch.end(),
std::back_inserter(added));
std::set_difference(query.lastMatch.begin(), query.lastMatch.end(), results.begin(), results.end(),
std::back_inserter(removed));
if (added.empty() && removed.empty())
continue;
std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, cmpSourcePosition->GetPosition2D()));
messages.resize(messages.size() + 1);
std::pair& back = messages.back();
back.first = query.source.GetId();
back.second.tag = it->first;
back.second.added.swap(added);
back.second.removed.swap(removed);
it->second.lastMatch.swap(results);
}
CComponentManager& cmpMgr = GetSimContext().GetComponentManager();
for (size_t i = 0; i < messages.size(); ++i)
cmpMgr.PostMessage(messages[i].first, messages[i].second);
}
/**
* Returns whether the given entity matches the given query (ignoring maxRange)
*/
bool TestEntityQuery(const Query& q, entity_id_t id, const EntityData& entity)
{
// Quick filter to ignore entities with the wrong owner
if (!(CalcOwnerMask(entity.owner) & q.ownersMask))
return false;
// Ignore entities not present in the world
if (!entity.inWorld)
return false;
// Ignore entities that don't match the current flags
if (!(entity.flags & q.flagsMask))
return false;
// Ignore self
if (id == q.source.GetId())
return false;
// Ignore if it's missing the required interface
if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface))
return false;
return true;
}
/**
* Returns a list of distinct entity IDs that match the given query, sorted by ID.
*/
void PerformQuery(const Query& q, std::vector& r, CFixedVector2D pos)
{
// Special case: range -1.0 means check all entities ignoring distance
if (q.maxRange == entity_pos_t::FromInt(-1))
{
for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it)
{
if (!TestEntityQuery(q, it->first, it->second))
continue;
r.push_back(it->first);
}
}
// Not the entire world, so check a parabolic range, or a regular range
else if (q.parabolic)
{
// elevationBonus is part of the 3D position, as the source is really that much heigher
CmpPtr cmpSourcePosition(q.source);
CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+
CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ;
// Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange
SpatialQueryArray ents;
m_Subdivision.GetNear(ents, pos, q.maxRange*2);
for (int i = 0; i < ents.size(); ++i)
{
EntityMap::const_iterator it = m_EntityData.find(ents[i]);
ENSURE(it != m_EntityData.end());
if (!TestEntityQuery(q, it->first, it->second))
continue;
CmpPtr cmpSecondPosition(GetSimContext(), ents[i]);
if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld())
continue;
CFixedVector3D secondPosition = cmpSecondPosition->GetPosition();
// Restrict based on precise distance
if (!InParabolicRange(
CFixedVector3D(it->second.x, secondPosition.Y, it->second.z)
- pos3d,
q.maxRange))
continue;
if (!q.minRange.IsZero())
{
int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange);
if (distVsMin < 0)
continue;
}
r.push_back(it->first);
}
}
// check a regular range (i.e. not the entire world, and not parabolic)
else
{
// Get a quick list of entities that are potentially in range
SpatialQueryArray ents;
m_Subdivision.GetNear(ents, pos, q.maxRange);
for (int i = 0; i < ents.size(); ++i)
{
EntityMap::const_iterator it = m_EntityData.find(ents[i]);
ENSURE(it != m_EntityData.end());
if (!TestEntityQuery(q, it->first, it->second))
continue;
// Restrict based on precise distance
int distVsMax = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange);
if (distVsMax > 0)
continue;
if (!q.minRange.IsZero())
{
int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange);
if (distVsMin < 0)
continue;
}
r.push_back(it->first);
}
}
}
virtual entity_pos_t GetElevationAdaptedRange(CFixedVector3D pos, CFixedVector3D rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle)
{
entity_pos_t r = entity_pos_t::Zero() ;
pos.Y += elevationBonus;
entity_pos_t orientation = rot.Y;
entity_pos_t maxAngle = orientation + angle/2;
entity_pos_t minAngle = orientation - angle/2;
int numberOfSteps = 16;
if (angle == entity_pos_t::Zero())
numberOfSteps = 1;
std::vector coords = getParabolicRangeForm(pos, range, range*2, minAngle, maxAngle, numberOfSteps);
entity_pos_t part = entity_pos_t::FromInt(numberOfSteps);
for (int i = 0; i < numberOfSteps; i++)
{
r = r + CFixedVector2D(coords[2*i],coords[2*i+1]).Length() / part;
}
return r;
}
virtual std::vector getParabolicRangeForm(CFixedVector3D pos, entity_pos_t maxRange, entity_pos_t cutoff, entity_pos_t minAngle, entity_pos_t maxAngle, int numberOfSteps)
{
// angle = 0 goes in the positive Z direction
entity_pos_t precision = entity_pos_t::FromInt((int)TERRAIN_TILE_SIZE)/8;
std::vector r;
CmpPtr cmpTerrain(GetSystemEntity());
CmpPtr cmpWaterManager(GetSystemEntity());
entity_pos_t waterLevel = cmpWaterManager->GetWaterLevel(pos.X,pos.Z);
entity_pos_t thisHeight = pos.Y > waterLevel ? pos.Y : waterLevel;
if (cmpTerrain)
{
for (int i = 0; i < numberOfSteps; i++)
{
entity_pos_t angle = minAngle + (maxAngle - minAngle) / numberOfSteps * i;
entity_pos_t sin;
entity_pos_t cos;
entity_pos_t minDistance = entity_pos_t::Zero();
entity_pos_t maxDistance = cutoff;
sincos_approx(angle,sin,cos);
CFixedVector2D minVector = CFixedVector2D(entity_pos_t::Zero(),entity_pos_t::Zero());
CFixedVector2D maxVector = CFixedVector2D(sin,cos).Multiply(cutoff);
entity_pos_t targetHeight = cmpTerrain->GetGroundLevel(pos.X+maxVector.X,pos.Z+maxVector.Y);
// use water level to display range on water
targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel;
if (InParabolicRange(CFixedVector3D(maxVector.X,targetHeight-thisHeight,maxVector.Y),maxRange))
{
r.push_back(maxVector.X);
r.push_back(maxVector.Y);
continue;
}
// Loop until vectors come close enough
while ((maxVector - minVector).CompareLength(precision) > 0)
{
// difference still bigger than precision, bisect to get smaller difference
entity_pos_t newDistance = (minDistance+maxDistance)/entity_pos_t::FromInt(2);
CFixedVector2D newVector = CFixedVector2D(sin,cos).Multiply(newDistance);
// get the height of the ground
targetHeight = cmpTerrain->GetGroundLevel(pos.X+newVector.X,pos.Z+newVector.Y);
targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel;
if (InParabolicRange(CFixedVector3D(newVector.X,targetHeight-thisHeight,newVector.Y),maxRange))
{
// new vector is in parabolic range, so this is a new minVector
minVector = newVector;
minDistance = newDistance;
}
else
{
// new vector is out parabolic range, so this is a new maxVector
maxVector = newVector;
maxDistance = newDistance;
}
}
r.push_back(maxVector.X);
r.push_back(maxVector.Y);
}
r.push_back(r[0]);
r.push_back(r[1]);
}
return r;
}
Query ConstructQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange,
const std::vector& owners, int requiredInterface, u8 flagsMask)
{
// Min range must be non-negative
if (minRange < entity_pos_t::Zero())
LOGWARNING(L"CCmpRangeManager: Invalid min range %f in query for entity %u", minRange.ToDouble(), source);
// Max range must be non-negative, or else -1
if (maxRange < entity_pos_t::Zero() && maxRange != entity_pos_t::FromInt(-1))
LOGWARNING(L"CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source);
Query q;
q.enabled = false;
q.parabolic = false;
q.source = GetSimContext().GetComponentManager().LookupEntityHandle(source);
q.minRange = minRange;
q.maxRange = maxRange;
q.elevationBonus = entity_pos_t::Zero();
q.ownersMask = 0;
for (size_t i = 0; i < owners.size(); ++i)
q.ownersMask |= CalcOwnerMask(owners[i]);
q.interface = requiredInterface;
q.flagsMask = flagsMask;
return q;
}
Query ConstructParabolicQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus,
const std::vector& owners, int requiredInterface, u8 flagsMask)
{
Query q = ConstructQuery(source,minRange,maxRange,owners,requiredInterface,flagsMask);
q.parabolic = true;
q.elevationBonus = elevationBonus;
return q;
}
void RenderSubmit(SceneCollector& collector)
{
if (!m_DebugOverlayEnabled)
return;
static CColor disabledRingColour(1, 0, 0, 1); // red
static CColor enabledRingColour(0, 1, 0, 1); // green
static CColor subdivColour(0, 0, 1, 1); // blue
static CColor rayColour(1, 1, 0, 0.2f);
if (m_DebugOverlayDirty)
{
m_DebugOverlayLines.clear();
for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it)
{
Query& q = it->second;
CmpPtr cmpSourcePosition(q.source);
if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld())
continue;
CFixedVector2D pos = cmpSourcePosition->GetPosition2D();
// Draw the max range circle
if (!q.parabolic)
{
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour);
SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true);
}
else
{
// elevation bonus is part of the 3D position. As if the unit is really that much higher
CFixedVector3D pos = cmpSourcePosition->GetPosition();
pos.Y += q.elevationBonus;
std::vector coords;
// Get the outline from cache if possible
if (ParabolicRangesOutlines.find(q.source.GetId()) != ParabolicRangesOutlines.end())
{
EntityParabolicRangeOutline e = ParabolicRangesOutlines[q.source.GetId()];
if (e.position == pos && e.range == q.maxRange)
{
// outline is cached correctly, use it
coords = e.outline;
}
else
{
// outline was cached, but important parameters changed
// (position, elevation, range)
// update it
coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70);
e.outline = coords;
e.range = q.maxRange;
e.position = pos;
ParabolicRangesOutlines[q.source.GetId()] = e;
}
}
else
{
// outline wasn't cached (first time you enable the range overlay
// or you created a new entiy)
// cache a new outline
coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70);
EntityParabolicRangeOutline e;
e.source = q.source.GetId();
e.range = q.maxRange;
e.position = pos;
e.outline = coords;
ParabolicRangesOutlines[q.source.GetId()] = e;
}
CColor thiscolor = q.enabled ? enabledRingColour : disabledRingColour;
// draw the outline (piece by piece)
for (size_t i = 3; i < coords.size(); i += 2)
{
std::vector c;
c.push_back((coords[i-3]+pos.X).ToFloat());
c.push_back((coords[i-2]+pos.Z).ToFloat());
c.push_back((coords[i-1]+pos.X).ToFloat());
c.push_back((coords[i]+pos.Z).ToFloat());
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = thiscolor;
SimRender::ConstructLineOnGround(GetSimContext(), c, m_DebugOverlayLines.back(), true);
}
}
// Draw the min range circle
if (!q.minRange.IsZero())
{
SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.minRange.ToFloat(), m_DebugOverlayLines.back(), true);
}
// Draw a ray from the source to each matched entity
for (size_t i = 0; i < q.lastMatch.size(); ++i)
{
CmpPtr cmpTargetPosition(GetSimContext(), q.lastMatch[i]);
if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
continue;
CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D();
std::vector coords;
coords.push_back(pos.X.ToFloat());
coords.push_back(pos.Y.ToFloat());
coords.push_back(targetPos.X.ToFloat());
coords.push_back(targetPos.Y.ToFloat());
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = rayColour;
SimRender::ConstructLineOnGround(GetSimContext(), coords, m_DebugOverlayLines.back(), true);
}
}
// render subdivision grid
float divSize = m_Subdivision.GetDivisionSize().ToFloat();
int width = m_Subdivision.GetWidth();
int height = m_Subdivision.GetHeight();
for (int x = 0; x < width; ++x)
{
for (int y = 0; y < height; ++y)
{
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = subdivColour;
float xpos = x*divSize + divSize/2;
float zpos = y*divSize + divSize/2;
SimRender::ConstructSquareOnGround(GetSimContext(), xpos, zpos, divSize, divSize, 0.0f,
m_DebugOverlayLines.back(), false, 1.0f);
}
}
m_DebugOverlayDirty = false;
}
for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i)
collector.Submit(&m_DebugOverlayLines[i]);
}
virtual u8 GetEntityFlagMask(std::string identifier)
{
if (identifier == "normal")
return 1;
if (identifier == "injured")
return 2;
LOGWARNING(L"CCmpRangeManager: Invalid flag identifier %hs", identifier.c_str());
return 0;
}
virtual void SetEntityFlag(entity_id_t ent, std::string identifier, bool value)
{
EntityMap::iterator it = m_EntityData.find(ent);
// We don't have this entity
if (it == m_EntityData.end())
return;
u8 flag = GetEntityFlagMask(identifier);
// We don't have a flag set
if (flag == 0)
{
LOGWARNING(L"CCmpRangeManager: Invalid flag identifier %hs for entity %u", identifier.c_str(), ent);
return;
}
if (value)
it->second.flags |= flag;
else
it->second.flags &= ~flag;
}
// ****************************************************************
// LOS implementation:
virtual CLosQuerier GetLosQuerier(player_id_t player)
{
if (GetLosRevealAll(player))
return CLosQuerier(0xFFFFFFFFu, m_LosStateRevealed, m_TerrainVerticesPerSide);
else
return CLosQuerier(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide);
}
virtual ELosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player, bool forceRetainInFog)
{
// This function provides the real visibility of any entity (even local ones) at any time.
// The m_EntityData visibility is updated at most once per turn with this function's return value
// and must not be used for rendering
// Entities not with positions in the world are never visible
if (ent.GetId() == INVALID_ENTITY)
return VIS_HIDDEN;
CmpPtr cmpPosition(ent);
if (!cmpPosition || !cmpPosition->IsInWorld())
return VIS_HIDDEN;
CFixedVector2D pos = cmpPosition->GetPosition2D();
int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest();
int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest();
// Reveal flag makes all positioned entities visible
if (GetLosRevealAll(player))
{
if (LosIsOffWorld(i, j))
return VIS_HIDDEN;
else
return VIS_VISIBLE;
}
+ // Mirage entities, whatever their position, are visible for one specific player
+ CmpPtr cmpMirage(ent);
+ if (cmpMirage && cmpMirage->GetPlayer() != player)
+ return VIS_HIDDEN;
+
// Visible if within a visible region
CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide);
if (los.IsVisible(i, j))
return VIS_VISIBLE;
+ if (!los.IsExplored(i, j))
+ return VIS_HIDDEN;
+
// Fogged if the 'retain in fog' flag is set, and in a non-visible explored region
- if (los.IsExplored(i, j))
+ CmpPtr cmpVision(ent);
+ if (!forceRetainInFog && !(cmpVision && cmpVision->GetRetainInFog()))
+ return VIS_HIDDEN;
+
+ if (cmpMirage && cmpMirage->GetPlayer() == player)
+ return VIS_FOGGED;
+
+ CmpPtr cmpOwnership(ent);
+ if (!cmpOwnership)
+ return VIS_VISIBLE;
+
+ if (cmpOwnership->GetOwner() == player)
{
- CmpPtr cmpVision(ent);
- if (forceRetainInFog || (cmpVision && cmpVision->GetRetainInFog()))
+ CmpPtr cmpFogging(ent);
+ if (!cmpFogging)
+ return VIS_VISIBLE;
+
+ // Fogged entities must not disappear while the mirage is not ready
+ if (!cmpFogging->IsMiraged(player))
return VIS_FOGGED;
- }
- // Otherwise not visible
+ return VIS_HIDDEN;
+ }
+
+ // Fogged entities must not disappear while the mirage is not ready
+ CmpPtr cmpFogging(ent);
+ if (cmpFogging && cmpFogging->WasSeen(player) && !cmpFogging->IsMiraged(player))
+ return VIS_FOGGED;
+
return VIS_HIDDEN;
}
virtual ELosVisibility GetLosVisibility(entity_id_t ent, player_id_t player, bool forceRetainInFog)
{
CEntityHandle handle = GetSimContext().GetComponentManager().LookupEntityHandle(ent);
return GetLosVisibility(handle, player, forceRetainInFog);
}
i32 PosToLosTilesHelper(entity_pos_t x, entity_pos_t z)
{
i32 i = Clamp(
(x/(entity_pos_t::FromInt(TERRAIN_TILE_SIZE * LOS_TILES_RATIO))).ToInt_RoundToZero(),
0,
m_LosTilesPerSide - 1);
i32 j = Clamp(
(z/(entity_pos_t::FromInt(TERRAIN_TILE_SIZE * LOS_TILES_RATIO))).ToInt_RoundToZero(),
0,
m_LosTilesPerSide - 1);
return j*m_LosTilesPerSide + i;
}
void AddToTile(i32 tile, entity_id_t ent)
{
m_LosTiles[tile].insert(ent);
}
void RemoveFromTile(i32 tile, entity_id_t ent)
{
for (std::set::iterator tileIt = m_LosTiles[tile].begin();
tileIt != m_LosTiles[tile].end();
++tileIt)
{
if (*tileIt == ent)
{
m_LosTiles[tile].erase(tileIt);
return;
}
}
}
void UpdateVisibilityData()
{
PROFILE("UpdateVisibilityData");
for (i32 n = 0; n < m_LosTilesPerSide*m_LosTilesPerSide; ++n)
{
if (m_DirtyVisibility[n] == 1)
{
for (std::set::iterator it = m_LosTiles[n].begin();
it != m_LosTiles[n].end();
++it)
{
UpdateVisibility(*it);
}
m_DirtyVisibility[n] = 0;
}
}
for (std::vector::iterator it = m_ModifiedEntities.begin(); it != m_ModifiedEntities.end(); ++it)
{
UpdateVisibility(*it);
}
m_ModifiedEntities.clear();
}
void UpdateVisibility(entity_id_t ent)
{
EntityMap::iterator itEnts = m_EntityData.find(ent);
if (itEnts == m_EntityData.end())
return;
+
+ std::vector oldVisibilities;
+ std::vector newVisibilities;
- for (player_id_t player = 1; player <= MAX_LOS_PLAYER_ID; ++player)
+ for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID+1; ++player)
{
u8 oldVis = (itEnts->second.visibilities >> (2*(player-1))) & 0x3;
u8 newVis = GetLosVisibility(itEnts->first, player, false);
+
+ oldVisibilities.push_back(oldVis);
+ newVisibilities.push_back(newVis);
if (oldVis != newVis)
- {
- CMessageVisibilityChanged msg(player, ent, oldVis, newVis);
- GetSimContext().GetComponentManager().PostMessage(ent, msg);
itEnts->second.visibilities = (itEnts->second.visibilities & ~(0x3 << 2*(player-1))) | (newVis << 2*(player-1));
- }
+ }
+
+ for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID+1; ++player)
+ {
+ if (oldVisibilities[player-1] == newVisibilities[player-1])
+ continue;
+
+ CMessageVisibilityChanged msg(player, ent, oldVisibilities[player-1], newVisibilities[player-1]);
+ GetSimContext().GetComponentManager().PostMessage(ent, msg);
}
}
virtual void SetLosRevealAll(player_id_t player, bool enabled)
{
if (player == -1)
m_LosRevealAll[MAX_LOS_PLAYER_ID+1] = enabled;
else
{
ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID);
m_LosRevealAll[player] = enabled;
}
}
virtual bool GetLosRevealAll(player_id_t player)
{
// Special player value can force reveal-all for every player
if (m_LosRevealAll[MAX_LOS_PLAYER_ID+1] || player == -1)
return true;
ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID+1);
// Otherwise check the player-specific flag
if (m_LosRevealAll[player])
return true;
return false;
}
virtual void SetLosCircular(bool enabled)
{
m_LosCircular = enabled;
ResetDerivedData(false);
}
virtual bool GetLosCircular()
{
return m_LosCircular;
}
virtual void SetSharedLos(player_id_t player, std::vector players)
{
m_SharedLosMasks[player] = CalcSharedLosMask(players);
}
virtual u32 GetSharedLosMask(player_id_t player)
{
return m_SharedLosMasks[player];
}
void ExploreAllTiles(player_id_t p)
{
for (u16 j = 0; j < m_TerrainVerticesPerSide; ++j)
{
for (u16 i = 0; i < m_TerrainVerticesPerSide; ++i)
{
if (LosIsOffWorld(i,j))
continue;
u32 &explored = m_ExploredVertices.at(p);
explored += !(m_LosState[i + j*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i + j*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
}
}
}
void UpdateTerritoriesLos()
{
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (!cmpTerritoryManager || !cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID))
return;
const Grid& grid = cmpTerritoryManager->GetTerritoryGrid();
ENSURE(grid.m_W == m_TerrainVerticesPerSide-1 && grid.m_H == m_TerrainVerticesPerSide-1);
// For each tile, if it is owned by a valid player then update the LOS
// for every vertex around that tile, to mark them as explored
for (u16 j = 0; j < grid.m_H; ++j)
{
for (u16 i = 0; i < grid.m_W; ++i)
{
u8 p = grid.get(i, j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK;
if (p > 0 && p <= MAX_LOS_PLAYER_ID)
{
u32 &explored = m_ExploredVertices.at(p);
explored += !(m_LosState[i + j*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i + j*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
explored += !(m_LosState[i+1 + j*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i+1 + j*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
explored += !(m_LosState[i + (j+1)*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i + (j+1)*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
explored += !(m_LosState[i+1 + (j+1)*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i+1 + (j+1)*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
}
}
}
}
/**
* Returns whether the given vertex is outside the normal bounds of the world
* (i.e. outside the range of a circular map)
*/
inline bool LosIsOffWorld(ssize_t i, ssize_t j)
{
// WARNING: CCmpObstructionManager::Rasterise needs to be kept in sync with this
const ssize_t edgeSize = 3; // number of vertexes around the edge that will be off-world
if (m_LosCircular)
{
// With a circular map, vertex is off-world if hypot(i - size/2, j - size/2) >= size/2:
ssize_t dist2 = (i - m_TerrainVerticesPerSide/2)*(i - m_TerrainVerticesPerSide/2)
+ (j - m_TerrainVerticesPerSide/2)*(j - m_TerrainVerticesPerSide/2);
ssize_t r = m_TerrainVerticesPerSide/2 - edgeSize + 1;
// subtract a bit from the radius to ensure nice
// SoD blurring around the edges of the map
return (dist2 >= r*r);
}
else
{
// With a square map, the outermost edge of the map should be off-world,
// so the SoD texture blends out nicely
return (i < edgeSize || j < edgeSize || i >= m_TerrainVerticesPerSide-edgeSize || j >= m_TerrainVerticesPerSide-edgeSize);
}
}
/**
* Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive).
*/
inline void LosAddStripHelper(u8 owner, i32 i0, i32 i1, i32 j, u16* counts)
{
if (i1 < i0)
return;
i32 idx0 = j*m_TerrainVerticesPerSide + i0;
i32 idx1 = j*m_TerrainVerticesPerSide + i1;
u32 &explored = m_ExploredVertices.at(owner);
for (i32 idx = idx0; idx <= idx1; ++idx)
{
// Increasing from zero to non-zero - move from unexplored/explored to visible+explored
if (counts[idx] == 0)
{
i32 i = i0 + idx - idx0;
if (!LosIsOffWorld(i, j))
{
explored += !(m_LosState[idx] & (LOS_EXPLORED << (2*(owner-1))));
m_LosState[idx] |= ((LOS_VISIBLE | LOS_EXPLORED) << (2*(owner-1)));
}
m_DirtyVisibility[(j/LOS_TILES_RATIO)*m_LosTilesPerSide + i/LOS_TILES_RATIO] = 1;
}
ASSERT(counts[idx] < 65535);
counts[idx] = (u16)(counts[idx] + 1); // ignore overflow; the player should never have 64K units
}
}
/**
* Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive).
*/
inline void LosRemoveStripHelper(u8 owner, i32 i0, i32 i1, i32 j, u16* counts)
{
if (i1 < i0)
return;
i32 idx0 = j*m_TerrainVerticesPerSide + i0;
i32 idx1 = j*m_TerrainVerticesPerSide + i1;
for (i32 idx = idx0; idx <= idx1; ++idx)
{
ASSERT(counts[idx] > 0);
counts[idx] = (u16)(counts[idx] - 1);
// Decreasing from non-zero to zero - move from visible+explored to explored
if (counts[idx] == 0)
{
// (If LosIsOffWorld then this is a no-op, so don't bother doing the check)
m_LosState[idx] &= ~(LOS_VISIBLE << (2*(owner-1)));
i32 i = i0 + idx - idx0;
m_DirtyVisibility[(j/LOS_TILES_RATIO)*m_LosTilesPerSide + i/LOS_TILES_RATIO] = 1;
}
}
}
/**
* Update the LOS state of tiles within a given circular range,
* either adding or removing visibility depending on the template parameter.
* Assumes owner is in the valid range.
*/
template
void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos)
{
if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet
return;
PROFILE("LosUpdateHelper");
std::vector& counts = m_LosPlayerCounts.at(owner);
// Lazy initialisation of counts:
if (counts.empty())
counts.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide);
u16* countsData = &counts[0];
// Compute the circular region as a series of strips.
// Rather than quantise pos to vertexes, we do more precise sub-tile computations
// to get smoother behaviour as a unit moves rather than jumping a whole tile
// at once.
// To avoid the cost of sqrt when computing the outline of the circle,
// we loop from the bottom to the top and estimate the width of the current
// strip based on the previous strip, then adjust each end of the strip
// inwards or outwards until it's the widest that still falls within the circle.
// Compute top/bottom coordinates, and clamp to exclude the 1-tile border around the map
// (so that we never render the sharp edge of the map)
i32 j0 = ((pos.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity();
i32 j1 = ((pos.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity();
i32 j0clamp = std::max(j0, 1);
i32 j1clamp = std::min(j1, m_TerrainVerticesPerSide-2);
// Translate world coordinates into fractional tile-space coordinates
entity_pos_t x = pos.X / (int)TERRAIN_TILE_SIZE;
entity_pos_t y = pos.Y / (int)TERRAIN_TILE_SIZE;
entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE;
entity_pos_t r2 = r.Square();
// Compute the integers on either side of x
i32 xfloor = (x - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity();
i32 xceil = (x + entity_pos_t::Epsilon()).ToInt_RoundToInfinity();
// Initialise the strip (i0, i1) to a rough guess
i32 i0 = xfloor;
i32 i1 = xceil;
for (i32 j = j0clamp; j <= j1clamp; ++j)
{
// Adjust i0 and i1 to be the outermost values that don't exceed
// the circle's radius (i.e. require dy^2 + dx^2 <= r^2).
// When moving the points inwards, clamp them to xceil+1 or xfloor-1
// so they don't accidentally shoot off in the wrong direction forever.
entity_pos_t dy = entity_pos_t::FromInt(j) - y;
entity_pos_t dy2 = dy.Square();
while (dy2 + (entity_pos_t::FromInt(i0-1) - x).Square() <= r2)
--i0;
while (i0 < xceil && dy2 + (entity_pos_t::FromInt(i0) - x).Square() > r2)
++i0;
while (dy2 + (entity_pos_t::FromInt(i1+1) - x).Square() <= r2)
++i1;
while (i1 > xfloor && dy2 + (entity_pos_t::FromInt(i1) - x).Square() > r2)
--i1;
#if DEBUG_RANGE_MANAGER_BOUNDS
if (i0 <= i1)
{
ENSURE(dy2 + (entity_pos_t::FromInt(i0) - x).Square() <= r2);
ENSURE(dy2 + (entity_pos_t::FromInt(i1) - x).Square() <= r2);
}
ENSURE(dy2 + (entity_pos_t::FromInt(i0 - 1) - x).Square() > r2);
ENSURE(dy2 + (entity_pos_t::FromInt(i1 + 1) - x).Square() > r2);
#endif
// Clamp the strip to exclude the 1-tile border,
// then add or remove the strip as requested
i32 i0clamp = std::max(i0, 1);
i32 i1clamp = std::min(i1, m_TerrainVerticesPerSide-2);
if (adding)
LosAddStripHelper(owner, i0clamp, i1clamp, j, countsData);
else
LosRemoveStripHelper(owner, i0clamp, i1clamp, j, countsData);
}
}
/**
* Update the LOS state of tiles within a given circular range,
* by removing visibility around the 'from' position
* and then adding visibility around the 'to' position.
*/
void LosUpdateHelperIncremental(u8 owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to)
{
if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet
return;
PROFILE("LosUpdateHelperIncremental");
std::vector& counts = m_LosPlayerCounts.at(owner);
// Lazy initialisation of counts:
if (counts.empty())
counts.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide);
u16* countsData = &counts[0];
// See comments in LosUpdateHelper.
// This does exactly the same, except computing the strips for
// both circles simultaneously.
// (The idea is that the circles will be heavily overlapping,
// so we can compute the difference between the removed/added strips
// and only have to touch tiles that have a net change.)
i32 j0_from = ((from.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity();
i32 j1_from = ((from.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity();
i32 j0_to = ((to.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity();
i32 j1_to = ((to.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity();
i32 j0clamp = std::max(std::min(j0_from, j0_to), 1);
i32 j1clamp = std::min(std::max(j1_from, j1_to), m_TerrainVerticesPerSide-2);
entity_pos_t x_from = from.X / (int)TERRAIN_TILE_SIZE;
entity_pos_t y_from = from.Y / (int)TERRAIN_TILE_SIZE;
entity_pos_t x_to = to.X / (int)TERRAIN_TILE_SIZE;
entity_pos_t y_to = to.Y / (int)TERRAIN_TILE_SIZE;
entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE;
entity_pos_t r2 = r.Square();
i32 xfloor_from = (x_from - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity();
i32 xceil_from = (x_from + entity_pos_t::Epsilon()).ToInt_RoundToInfinity();
i32 xfloor_to = (x_to - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity();
i32 xceil_to = (x_to + entity_pos_t::Epsilon()).ToInt_RoundToInfinity();
i32 i0_from = xfloor_from;
i32 i1_from = xceil_from;
i32 i0_to = xfloor_to;
i32 i1_to = xceil_to;
for (i32 j = j0clamp; j <= j1clamp; ++j)
{
entity_pos_t dy_from = entity_pos_t::FromInt(j) - y_from;
entity_pos_t dy2_from = dy_from.Square();
while (dy2_from + (entity_pos_t::FromInt(i0_from-1) - x_from).Square() <= r2)
--i0_from;
while (i0_from < xceil_from && dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() > r2)
++i0_from;
while (dy2_from + (entity_pos_t::FromInt(i1_from+1) - x_from).Square() <= r2)
++i1_from;
while (i1_from > xfloor_from && dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() > r2)
--i1_from;
entity_pos_t dy_to = entity_pos_t::FromInt(j) - y_to;
entity_pos_t dy2_to = dy_to.Square();
while (dy2_to + (entity_pos_t::FromInt(i0_to-1) - x_to).Square() <= r2)
--i0_to;
while (i0_to < xceil_to && dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() > r2)
++i0_to;
while (dy2_to + (entity_pos_t::FromInt(i1_to+1) - x_to).Square() <= r2)
++i1_to;
while (i1_to > xfloor_to && dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() > r2)
--i1_to;
#if DEBUG_RANGE_MANAGER_BOUNDS
if (i0_from <= i1_from)
{
ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() <= r2);
ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() <= r2);
}
ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from - 1) - x_from).Square() > r2);
ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from + 1) - x_from).Square() > r2);
if (i0_to <= i1_to)
{
ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() <= r2);
ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() <= r2);
}
ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to - 1) - x_to).Square() > r2);
ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to + 1) - x_to).Square() > r2);
#endif
// Check whether this strip moved at all
if (!(i0_to == i0_from && i1_to == i1_from))
{
i32 i0clamp_from = std::max(i0_from, 1);
i32 i1clamp_from = std::min(i1_from, m_TerrainVerticesPerSide-2);
i32 i0clamp_to = std::max(i0_to, 1);
i32 i1clamp_to = std::min(i1_to, m_TerrainVerticesPerSide-2);
// Check whether one strip is negative width,
// and we can just add/remove the entire other strip
if (i1clamp_from < i0clamp_from)
{
LosAddStripHelper(owner, i0clamp_to, i1clamp_to, j, countsData);
}
else if (i1clamp_to < i0clamp_to)
{
LosRemoveStripHelper(owner, i0clamp_from, i1clamp_from, j, countsData);
}
else
{
// There are four possible regions of overlap between the two strips
// (remove before add, remove after add, add before remove, add after remove).
// Process each of the regions as its own strip.
// (If this produces negative-width strips then they'll just get ignored
// which is fine.)
// (If the strips don't actually overlap (which is very rare with normal unit
// movement speeds), the region between them will be both added and removed,
// so we have to do the add first to avoid overflowing to -1 and triggering
// assertion failures.)
LosAddStripHelper(owner, i0clamp_to, i0clamp_from-1, j, countsData);
LosAddStripHelper(owner, i1clamp_from+1, i1clamp_to, j, countsData);
LosRemoveStripHelper(owner, i0clamp_from, i0clamp_to-1, j, countsData);
LosRemoveStripHelper(owner, i1clamp_to+1, i1clamp_from, j, countsData);
}
}
}
}
void LosAdd(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos)
{
if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID)
return;
LosUpdateHelper((u8)owner, visionRange, pos);
}
void LosRemove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos)
{
if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID)
return;
LosUpdateHelper((u8)owner, visionRange, pos);
}
void LosMove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to)
{
if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID)
return;
if ((from - to).CompareLength(visionRange) > 0)
{
// If it's a very large move, then simply remove and add to the new position
LosUpdateHelper((u8)owner, visionRange, from);
LosUpdateHelper((u8)owner, visionRange, to);
}
else
{
// Otherwise use the version optimised for mostly-overlapping circles
LosUpdateHelperIncremental((u8)owner, visionRange, from, to);
}
}
virtual u8 GetPercentMapExplored(player_id_t player)
{
return m_ExploredVertices.at((u8)player) * 100 / m_TotalInworldVertices;
}
};
REGISTER_COMPONENT_TYPE(RangeManager)
Index: ps/trunk/source/simulation2/components/ICmpFogging.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpFogging.cpp (nonexistent)
+++ ps/trunk/source/simulation2/components/ICmpFogging.cpp (revision 15612)
@@ -0,0 +1,44 @@
+/* Copyright (C) 2014 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "precompiled.h"
+
+#include "ICmpFogging.h"
+
+#include "simulation2/scripting/ScriptComponent.h"
+#include "simulation2/system/InterfaceScripted.h"
+
+BEGIN_INTERFACE_WRAPPER(Fogging)
+END_INTERFACE_WRAPPER(Fogging)
+
+class CCmpFoggingScripted : public ICmpFogging
+{
+public:
+ DEFAULT_SCRIPT_WRAPPER(FoggingScripted)
+
+ virtual bool WasSeen(player_id_t player)
+ {
+ return m_Script.Call("WasSeen", player);
+ }
+
+ virtual bool IsMiraged(player_id_t player)
+ {
+ return m_Script.Call("IsMiraged", player);
+ }
+};
+
+REGISTER_COMPONENT_SCRIPT_WRAPPER(FoggingScripted)
Index: ps/trunk/source/simulation2/components/ICmpFogging.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpFogging.h (nonexistent)
+++ ps/trunk/source/simulation2/components/ICmpFogging.h (revision 15612)
@@ -0,0 +1,39 @@
+/* Copyright (C) 2014 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_ICMPFOGGING
+#define INCLUDED_ICMPFOGGING
+
+#include "simulation2/system/Interface.h"
+
+#include "simulation2/helpers/Player.h"
+
+/**
+ * Handles the fogging of out-of-sight enemy entities, by creating mirage
+ * entities.
+ * This allows hiding changes, especially destruction status or health.
+ */
+class ICmpFogging : public IComponent
+{
+public:
+ virtual bool WasSeen(player_id_t player) = 0;
+ virtual bool IsMiraged(player_id_t player) = 0;
+
+ DECLARE_INTERFACE_TYPE(Fogging)
+};
+
+#endif // INCLUDED_ICMPFOGGING
Index: ps/trunk/source/simulation2/components/ICmpMirage.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpMirage.cpp (nonexistent)
+++ ps/trunk/source/simulation2/components/ICmpMirage.cpp (revision 15612)
@@ -0,0 +1,39 @@
+/* Copyright (C) 2014 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "precompiled.h"
+
+#include "ICmpMirage.h"
+
+#include "simulation2/scripting/ScriptComponent.h"
+#include "simulation2/system/InterfaceScripted.h"
+
+BEGIN_INTERFACE_WRAPPER(Mirage)
+END_INTERFACE_WRAPPER(Mirage)
+
+class CCmpMirageScripted : public ICmpMirage
+{
+public:
+ DEFAULT_SCRIPT_WRAPPER(MirageScripted)
+
+ virtual player_id_t GetPlayer()
+ {
+ return m_Script.Call("GetPlayer");
+ }
+};
+
+REGISTER_COMPONENT_SCRIPT_WRAPPER(MirageScripted)
Index: ps/trunk/source/simulation2/components/ICmpMirage.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpMirage.h (nonexistent)
+++ ps/trunk/source/simulation2/components/ICmpMirage.h (revision 15612)
@@ -0,0 +1,37 @@
+/* Copyright (C) 2014 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_ICMPMIRAGE
+#define INCLUDED_ICMPMIRAGE
+
+#include "simulation2/system/Interface.h"
+
+#include "simulation2/helpers/Player.h"
+
+/**
+ * Component allowing mirage entities to communicate with their parent entity.
+ * See ICmpFogging.
+ */
+class ICmpMirage : public IComponent
+{
+public:
+ virtual player_id_t GetPlayer() = 0;
+
+ DECLARE_INTERFACE_TYPE(Mirage)
+};
+
+#endif // INCLUDED_ICMPMIRAGE