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 @@ + gaia Gaia true true true true true false false 2.0 0.333 5.0 0 true false false true false 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 @@ 1 1 1 1 1 1 0 0 Ranged Infantry land own 0 0 10 0 0 0 0 0.0 3.0 9.8 + corpse 0 true true Structure Structure ConquestCritical structure true true true true true false false special/rallypoint art/textures/misc/rallypoint_line.png art/textures/misc/rallypoint_line_mask.png 0.2 square round default default outline_border.png outline_border_mask.png 0.4 interface/complete/building/complete_universal.xml attack/destruction/building_collapse_large.xml interface/alarm/alarm_attackplayer.xml attack/weapon/arrowfly.xml attack/impact/arrow_metal.xml 6.0 0.6 12.0 5 40 true false false true false 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