Index: binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- binaries/data/mods/public/globalscripts/Templates.js (revision 18426)
+++ binaries/data/mods/public/globalscripts/Templates.js (working copy)
@@ -233,7 +233,6 @@
};
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);
}
Index: binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- binaries/data/mods/public/gui/common/tooltips.js (revision 18426)
+++ binaries/data/mods/public/gui/common/tooltips.js (working copy)
@@ -288,7 +288,8 @@
trainNum = 1;
let totalCosts = multiplyEntityCosts(template, trainNum);
- totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", { "entity": entity, "batchSize": trainNum }) : 1));
+ if (template.cost.time)
+ totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", { "entity": entity, "batchSize": trainNum }) : 1));
let costs = [];
Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js (revision 18426)
+++ binaries/data/mods/public/gui/session/input.js (working copy)
@@ -1632,27 +1632,25 @@
});
}
-// Transform a wall to a gate
-function transformWallToGate(template)
+// Upgrade an entity to another
+function upgradeEntity(Template)
{
- var selection = g_Selection.toList();
Engine.PostNetworkCommand({
- "type": "wall-to-gate",
- "entities": selection.filter(e => getWallGateTemplate(e) == template),
- "template": template,
+ "type": "upgrade",
+ "entities": g_Selection.toList(),
+ "template": Template,
+ "queued": false
});
}
-// Gets the gate form (if any) of a given long wall piece
-function getWallGateTemplate(entity)
+// Cancel upgrading entities
+function cancelUpgradeEntity()
{
- // TODO: find the gate template name in a better way
- var entState = GetEntityState(entity);
- var index;
-
- if (entState && !entState.foundation && hasClass(entState, "LongWall") && (index = entState.template.indexOf("long")) >= 0)
- return entState.template.substr(0, index) + "gate";
- return undefined;
+ Engine.PostNetworkCommand({
+ "type": "cancel-upgrade",
+ "entities": g_Selection.toList(),
+ "queued": false
+ });
}
// Set the camera to follow the given unit
Index: binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_panels.js (revision 18426)
+++ binaries/data/mods/public/gui/session/selection_panels.js (working copy)
@@ -473,36 +473,12 @@
},
"getItems": function(unitEntState, selection)
{
- // Allow long wall pieces to be converted to gates
- let longWallTypes = {};
- let walls = [];
let gates = [];
for (let ent of selection)
{
let state = GetEntityState(ent);
- if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template])
+ if (state.gate && !gates.length)
{
- let gateTemplate = getWallGateTemplate(state.id);
- if (gateTemplate)
- {
- let tooltipString = GetTemplateDataWithoutLocalization(state.template).gateConversionTooltip;
- if (!tooltipString)
- {
- warn(state.template + " is supposed to be convertable to a gate, but it's missing the GateConversionTooltip in the Identity template");
- tooltipString = "";
- }
- walls.push({
- "tooltip": translate(tooltipString),
- "template": gateTemplate,
- "callback": function (item) { transformWallToGate(item.template); }
- });
- }
-
- // We only need one entity per type.
- longWallTypes[state.template] = true;
- }
- else if (state.gate && !gates.length)
- {
gates.push({
"gate": state.gate,
"tooltip": translate("Lock Gate"),
@@ -522,8 +498,7 @@
delete gates[j].gate.locked;
}
- // Place wall conversion options after gate lock/unlock icons.
- return gates.concat(walls);
+ return gates;
},
"setupButton": function(data)
{
@@ -530,32 +505,13 @@
data.button.onPress = function() {data.item.callback(data.item); };
let tooltip = data.item.tooltip;
- if (data.item.template)
- {
- data.template = GetTemplateData(data.item.template);
- data.wallCount = data.selection.reduce(function (count, ent) {
- let state = GetEntityState(ent);
- if (hasClass(state, "LongWall") && !state.gate)
- ++count;
- return count;
- }, 0);
+ data.button.tooltip = data.item.tooltip;
- tooltip += "\n" + getEntityCostTooltip(data.template, data.wallCount);
-
- data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
- "cost": multiplyEntityCosts(data.template, data.wallCount)
- });
-
- if (data.neededResources)
- tooltip += getNeededResourcesTooltip(data.neededResources);
- }
- data.button.tooltip = tooltip;
-
data.button.enabled = controlsPlayer(data.unitEntState.player);
let gateIcon;
if (data.item.gate)
{
- // If already a gate, show locking actions
+ // Show locking actions
gateIcon = "icons/lock_" + GATE_ACTIONS[data.item.locked ? 0 : 1] + "ed.png";
if (data.item.gate.locked === undefined)
data.guiSelection.hidden = false;
@@ -562,15 +518,6 @@
else
data.guiSelection.hidden = data.item.gate.locked != data.item.locked;
}
- else
- {
- // Otherwise show gate upgrade icon
- let template = GetTemplateData(data.item.template);
- if (!template)
- return false;
- gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png";
- data.guiSelection.hidden = true;
- }
data.icon.sprite = (data.neededResources ? resourcesToAlphaMask(data.neededResources) + ":" : "") + "stretched:session/" + gateIcon;
@@ -611,13 +558,33 @@
}
let items = [];
if (checks.packButton)
- items.push({ "packing": false, "packed": false, "tooltip": translate("Pack"), "callback": function() { packUnit(true); } });
+ items.push({
+ "packing": false,
+ "packed": false,
+ "tooltip": translate("Pack"),
+ "callback": function() { packUnit(true); }
+ });
if (checks.unpackButton)
- items.push({ "packing": false, "packed": true, "tooltip": translate("Unpack"), "callback": function() { packUnit(false); } });
+ items.push({
+ "packing": false,
+ "packed": true,
+ "tooltip": translate("Unpack"),
+ "callback": function() { packUnit(false); }
+ });
if (checks.packCancelButton)
- items.push({ "packing": true, "packed": false, "tooltip": translate("Cancel Packing"), "callback": function() { cancelPackUnit(true); } });
+ items.push({
+ "packing": true,
+ "packed": false,
+ "tooltip": translate("Cancel Packing"),
+ "callback": function() { cancelPackUnit(true); }
+ });
if (checks.unpackCancelButton)
- items.push({ "packing": true, "packed": true, "tooltip": translate("Cancel Unpacking"), "callback": function() { cancelPackUnit(false); } });
+ items.push({
+ "packing": true,
+ "packed": true,
+ "tooltip": translate("Cancel Unpacking"),
+ "callback": function() { cancelPackUnit(false); }
+ });
return items;
},
"setupButton": function(data)
@@ -1054,6 +1021,128 @@
}
};
+g_SelectionPanels.Upgrade = {
+ "getMaxNumberOfItems": function()
+ {
+ return 24 - getNumberOfRightPanelButtons();
+ },
+ "getItems": function(unitEntState, selection)
+ {
+ // Interface becomes complicated with multiple units and this is meant per-entity, so prevent it if the selection has multiple units.
+ // TODO: if the units are all the same, this should probably still be possible.
+ if (selection.length > 1)
+ return false;
+
+ if (!unitEntState.upgrade)
+ return false;
+
+ var items = [];
+
+ for (let upgrade of unitEntState.upgrade.upgrades)
+ {
+ var item = {
+ "entity": upgrade.entity,
+ "cost": upgrade.cost,
+ "time": upgrade.time,
+ "icon": upgrade.icon,
+ "tooltip": upgrade.tooltip,
+ "requiredTechnology": upgrade.requiredTechnology,
+ };
+ items.push(item);
+ }
+ return items;
+ },
+ "setupButton" : function(data)
+ {
+ let template = GetTemplateData(data.item.entity);
+ if (!template)
+ return false;
+
+ let technologyEnabled = true;
+
+ if (data.item.requiredTechnology)
+ technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
+ "tech": requiredTechnology,
+ "player": data.unitEntState.player
+ });
+
+ let neededResources;
+ if (data.item.cost)
+ neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
+ "cost": data.item.cost,
+ "player": data.unitEntState.player
+ });
+
+ let limits = getEntityLimitAndCount(data.playerState, data.item.entity);
+
+ let progress = data.unitEntState.upgrade.progress || 0;
+ let isUpgrading = data.unitEntState.upgrade.template == data.item.entity;
+
+ let tooltip;
+ if (!progress)
+ {
+ tooltip = sprintf(translate("Upgrade into a %(name)s.%(tooltip)s"), {
+ "name": template.name.generic,
+ "tooltip": data.item.tooltip ? "\n" + data.item.tooltip : ""
+ });
+ if (data.item.cost)
+ tooltip += "\n" + getEntityCostTooltip(data.item);
+
+ tooltip += formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers);
+ if (!technologyEnabled)
+ tooltip += "\n" + sprintf(translate("Requires %(technology)s"), {
+ "technology": getEntityNames(GetTechnologyData(data.item.requiredTechnology))
+ });
+ if (neededResources)
+ tooltip += getNeededResourcesTooltip(neededResources);
+
+ data.button.onPress = function() { upgradeEntity(data.item.entity); };
+ }
+ else if (isUpgrading)
+ {
+ tooltip = translate("Cancel Upgrading");
+ data.button.onPress = function() { cancelUpgradeEntity(); };
+ }
+ else
+ {
+ tooltip = translate("Cannot upgrade when the entity is already upgrading.");
+ data.button.onPress = function() {};
+ }
+ data.button.tooltip = tooltip;
+
+ let modifier = "";
+ if (!isUpgrading)
+ {
+ if (progress || !technologyEnabled || limits.canBeAddedCount == 0)
+ {
+ data.button.enabled = false;
+ modifier = "color:0 0 0 127:grayscale:";
+ }
+ else if (neededResources)
+ {
+ data.button.enabled = false;
+ modifier = resourcesToAlphaMask(neededResources) + ":";
+ }
+ }
+
+ data.icon.sprite = modifier + "stretched:session/" +
+ (data.item.icon || "portraits/" + template.icon);
+
+ let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]");
+ if (isUpgrading)
+ {
+ let size = progressOverlay.size;
+ size.top = size.left + Math.round(progress * (size.right - size.left));
+ progressOverlay.size = size;
+ }
+ progressOverlay.hidden = !isUpgrading;
+
+ let index = data.i + getNumberOfRightPanelButtons();
+ setPanelObjectPosition(data.button, index, data.rowLength);
+ return true;
+ },
+};
+
/**
* If two panels need the same space, so they collide,
* the one appearing first in the order is rendered.
@@ -1071,6 +1160,7 @@
// RIGHT PANE
"Gate", // Must always be shown on gates
"Pack", // Must always be shown on packable entities
+ "Upgrade", // Must always be shown on upgradable entities
"Training",
"Construction",
"Research", // Normal together with training
Index: binaries/data/mods/public/gui/session/selection_panels_helpers.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 18426)
+++ binaries/data/mods/public/gui/session/selection_panels_helpers.js (working copy)
@@ -8,6 +8,10 @@
// Gate constants
const GATE_ACTIONS = ["lock", "unlock"];
+// upgrade constants
+const UPGRADING_NOT_STARTED = -2;
+const UPGRADING_CHOSEN_OTHER = -1;
+
// ==============================================
// BARTER HELPERS
// Resources to sell on barter panel
Index: binaries/data/mods/public/gui/session/selection_panels_right/upgrade_panel.xml
===================================================================
--- binaries/data/mods/public/gui/session/selection_panels_right/upgrade_panel.xml (nonexistent)
+++ binaries/data/mods/public/gui/session/selection_panels_right/upgrade_panel.xml (working copy)
@@ -0,0 +1,15 @@
+
+
Property changes on: binaries/data/mods/public/gui/session/selection_panels_right/upgrade_panel.xml
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+text/xml
\ No newline at end of property
Index: binaries/data/mods/public/gui/session/unit_commands.js
===================================================================
--- binaries/data/mods/public/gui/session/unit_commands.js (revision 18426)
+++ binaries/data/mods/public/gui/session/unit_commands.js (working copy)
@@ -1,5 +1,21 @@
// The number of currently visible buttons (used to optimise showing/hiding)
-var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Research": 0, "Alert": 0, "Barter": 0, "Construction": 0, "Command": 0, "AllyCommand": 0, "Stance": 0, "Gate": 0, "Pack": 0};
+var g_unitPanelButtons = {
+ "Selection": 0,
+ "Queue": 0,
+ "Formation": 0,
+ "Garrison": 0,
+ "Training": 0,
+ "Research": 0,
+ "Alert": 0,
+ "Barter": 0,
+ "Construction": 0,
+ "Command": 0,
+ "AllyCommand": 0,
+ "Stance": 0,
+ "Gate":0,
+ "Pack": 0,
+ "Upgrade": 0
+};
/**
* Set the position of a panel object according to the index,
@@ -225,7 +241,7 @@
{
var sum = 0;
- for (let prop of ["Construction", "Training", "Pack", "Gate"])
+ for (let prop of ["Construction", "Training", "Pack", "Gate", "Upgrade"])
if (g_SelectionPanels[prop].used)
sum += g_unitPanelButtons[prop];
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js (revision 18426)
+++ binaries/data/mods/public/simulation/components/GuiInterface.js (working copy)
@@ -237,6 +237,7 @@
"market": null,
"mirage": null,
"pack": null,
+ "upgrade" : null,
"player": -1,
"position": null,
"production": null,
@@ -303,6 +304,14 @@
"progress": cmpPack.GetProgress(),
};
+ var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
+ if (cmpUpgrade)
+ ret.upgrade = {
+ "upgrades" : cmpUpgrade.GetUpgrades(),
+ "progress": cmpUpgrade.GetProgress(),
+ "template": cmpUpgrade.GetUpgradingTo()
+ };
+
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
Index: binaries/data/mods/public/simulation/components/Identity.js
===================================================================
--- binaries/data/mods/public/simulation/components/Identity.js (revision 18426)
+++ binaries/data/mods/public/simulation/components/Identity.js (working copy)
@@ -37,19 +37,14 @@
"" +
"" +
"" +
- "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
"" +
"" +
"" +
Index: binaries/data/mods/public/simulation/components/interfaces/Upgrade.js
===================================================================
--- binaries/data/mods/public/simulation/components/interfaces/Upgrade.js (nonexistent)
+++ binaries/data/mods/public/simulation/components/interfaces/Upgrade.js (working copy)
@@ -0,0 +1 @@
+Engine.RegisterInterface("Upgrade");
Index: binaries/data/mods/public/simulation/components/Pack.js
===================================================================
--- binaries/data/mods/public/simulation/components/Pack.js (revision 18426)
+++ binaries/data/mods/public/simulation/components/Pack.js (working copy)
@@ -116,87 +116,24 @@
Pack.prototype.PackProgress = function(data, lateness)
{
- if (this.elapsedTime >= this.GetPackTime())
+ if (this.elapsedTime < this.GetPackTime())
{
+ this.SetElapsedTime(this.GetElapsedTime() + PACKING_INTERVAL + lateness);
+ return;
+ }
+
this.CancelTimer();
this.packed = !this.packed;
- this.packing = false;
Engine.PostMessage(this.entity, MT_PackFinished, { packed: this.packed });
- // Done un/packing, copy our parameters to the final entity
- var newEntity = Engine.AddEntity(this.template.Entity);
- if (newEntity == INVALID_ENTITY)
- {
- // Error (e.g. invalid template names)
- error("PackProgress: Error creating entity for '" + this.template.Entity + "'");
- return;
- }
+ var newEntity = ChangeEntityTemplate(this.entity, this.template.Entity);
- var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- var cmpNewPosition = Engine.QueryInterface(newEntity, IID_Position);
- if (cmpPosition.IsInWorld())
- {
- var pos = cmpPosition.GetPosition2D();
- cmpNewPosition.JumpTo(pos.x, pos.y);
- }
- var rot = cmpPosition.GetRotation();
- cmpNewPosition.SetYRotation(rot.y);
- cmpNewPosition.SetXZRotation(rot.x, rot.z);
- cmpNewPosition.SetHeightOffset(cmpPosition.GetHeightOffset());
-
- var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
- var cmpNewOwnership = Engine.QueryInterface(newEntity, IID_Ownership);
- cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
-
- // rescale capture points
- var cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable);
- var cmpNewCapturable = Engine.QueryInterface(newEntity, IID_Capturable);
- if (cmpCapturable && cmpNewCapturable)
- {
- let scale = cmpCapturable.GetMaxCapturePoints() / cmpNewCapturable.GetMaxCapturePoints();
- let newCp = cmpCapturable.GetCapturePoints().map(function (v) { return v / scale; });
- cmpNewCapturable.SetCapturePoints(newCp);
- }
-
- // Maintain current health level
- var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
- var cmpNewHealth = Engine.QueryInterface(newEntity, IID_Health);
- var healthLevel = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
- cmpNewHealth.SetHitpoints(Math.round(cmpNewHealth.GetMaxHitpoints() * healthLevel));
-
- var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
- var cmpNewUnitAI = Engine.QueryInterface(newEntity, IID_UnitAI);
- if (cmpUnitAI && cmpNewUnitAI)
- {
- var pos = cmpUnitAI.GetHeldPosition();
- if (pos)
- cmpNewUnitAI.SetHeldPosition(pos.x, pos.z);
- if (cmpUnitAI.GetStanceName())
- cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName());
- cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders());
- cmpNewUnitAI.SetGuardOf(cmpUnitAI.IsGuardOf());
- }
-
- // Maintain the list of guards
- var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
- var cmpNewGuard = Engine.QueryInterface(newEntity, IID_Guard);
- if (cmpGuard && cmpNewGuard)
- cmpNewGuard.SetEntities(cmpGuard.GetEntities());
-
- Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: newEntity });
-
- // Play notification sound
+ if (newEntity)
+ {
var sound = this.packed ? "packed" : "unpacked";
PlaySound(sound, newEntity);
-
- // Destroy current entity
- Engine.DestroyEntity(this.entity);
}
- else
- {
- this.SetElapsedTime(this.GetElapsedTime() + PACKING_INTERVAL + lateness);
- }
};
Engine.RegisterComponentType(IID_Pack, "Pack", Pack);
Index: binaries/data/mods/public/simulation/components/Upgrade.js
===================================================================
--- binaries/data/mods/public/simulation/components/Upgrade.js (nonexistent)
+++ binaries/data/mods/public/simulation/components/Upgrade.js (working copy)
@@ -0,0 +1,306 @@
+function Upgrade() {}
+
+const UPGRADING_PROGRESS_INTERVAL = 250;
+
+Upgrade.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
+Upgrade.prototype.Init = function()
+{
+ this.upgrading = false;
+ this.elapsedTime = 0;
+ this.timer = undefined;
+
+ this.upgradeTemplates = {};
+
+ for (let choice in this.template)
+ {
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+ let name = this.template[choice].Entity;
+ if (cmpIdentity)
+ name = name.replace(/\{civ\}/g, cmpIdentity.GetCiv());
+ if (this.upgradeTemplates.name)
+ warn("Upgrade Component: entity " + this.entity + " has two upgrades to the same entity, only the last will be used.");
+ this.upgradeTemplates[name] = choice;
+ }
+};
+
+// On owner change, abort the upgrade
+// This will also deal with the "OnDestroy" case.
+Upgrade.prototype.OnOwnershipChanged = function(msg)
+{
+ this.CancelUpgrade();
+
+ if (msg.to !== -1)
+ this.owner = msg.to;
+};
+
+Upgrade.prototype.ChangeUpgradedEntityCount = function(amount)
+{
+ if (!this.IsUpgrading())
+ return;
+
+ var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ var template = cmpTempMan.GetTemplate(this.upgrading);
+
+ var category;
+ if (template.TrainingRestrictions)
+ category = template.TrainingRestrictions.Category;
+ else if (template.BuildRestrictions)
+ category = template.BuildRestrictions.Category;
+
+ if (!category)
+ return;
+
+ var cmpEntityLimits = QueryPlayerIDInterface(this.owner, IID_EntityLimits);
+ cmpEntityLimits.ChangeCount(category, amount);
+};
+
+Upgrade.prototype.CanUpgradeTo = function(template)
+{
+ return this.upgradeTemplates[template] !== undefined;
+};
+
+Upgrade.prototype.GetUpgrades = function()
+{
+ var ret = [];
+
+ var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+
+ for (let option in this.template)
+ {
+ let choice = this.template[option];
+ let entType = choice.Entity;
+ if (cmpIdentity)
+ entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv());
+
+ let hasCosts;
+ let cost = {};
+ if (choice.Cost)
+ {
+ hasCosts = true;
+ for (let type in choice.Cost)
+ cost[type] = ApplyValueModificationsToTemplate("Upgrade/Cost/"+type, +choice.Cost[type], this.owner, entType);
+ }
+ if (choice.Time)
+ {
+ hasCosts = true;
+ cost.time = ApplyValueModificationsToTemplate("Upgrade/Time", +choice.Time/1000.0, this.owner, entType);
+ }
+ ret.push({
+ "entity": entType,
+ "icon": choice.Icon || undefined,
+ "cost": hasCosts,
+ "tooltip": choice.Tooltip || undefined,
+ "requiredTechnology": this.GetRequiredTechnology(option),
+ });
+ }
+
+ return ret;
+};
+
+Upgrade.prototype.CancelTimer = function()
+{
+ if (!this.timer)
+ return;
+
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ this.timer = undefined;
+};
+
+Upgrade.prototype.IsUpgrading = function()
+{
+ return !!this.upgrading;
+};
+
+Upgrade.prototype.GetUpgradingTo = function()
+{
+ return this.upgrading;
+};
+
+Upgrade.prototype.WillCheckPlacementRestrictions = function(template)
+{
+ if (!this.upgradeTemplates[template])
+ return undefined;
+
+ // is undefined by default so use X in Y
+ return "CheckPlacementRestrictions" in this.template[this.upgradeTemplates[template]];
+};
+
+Upgrade.prototype.GetRequiredTechnology = function(templateArg)
+{
+ let choice = this.upgradeTemplates[templateArg] || templateArg
+
+ if (this.template[choice].RequiredTechnology)
+ return this.template[choice].RequiredTechnology;
+
+ if (!("RequiredTechnology" in this.template[choice]))
+ return undefined;
+
+ let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+
+ let entType = this.template[choice].Entity;
+ if (cmpIdentity)
+ entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv());
+
+ let template = cmpTemplateManager.GetTemplate(entType);
+ if (template.Identity.RequiredTechnology)
+ return template.Identity.RequiredTechnology;
+
+ return undefined;
+};
+
+Upgrade.prototype.GetResourceCosts = function(template)
+{
+ if (!this.upgradeTemplates[template])
+ return undefined;
+
+ var choice = this.upgradeTemplates[template];
+ if (!this.template[choice].Cost)
+ return {};
+
+ var costs = {};
+ for (let r in this.template[choice].Cost)
+ {
+ costs[r] = ApplyValueModificationsToEntity("Upgrade/Cost/"+r, +this.template[choice].Cost[r], this.entity);
+ }
+ return costs;
+};
+
+Upgrade.prototype.Upgrade = function(template)
+{
+ if (this.IsUpgrading() || !this.upgradeTemplates[template])
+ return false;
+
+ var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+
+ if (!cmpPlayer.TrySubtractResources(this.GetResourceCosts(template)))
+ return false;
+
+ this.upgrading = template;
+
+ // Prevent cheating
+ this.ChangeUpgradedEntityCount(1);
+
+ if (this.GetUpgradeTime(template) !== 0)
+ {
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetInterval(this.entity, IID_Upgrade, "UpgradeProgress", 0, UPGRADING_PROGRESS_INTERVAL, { "upgrading": template });
+ }
+ else
+ this.UpgradeProgress();
+
+ return true;
+};
+
+Upgrade.prototype.CancelUpgrade = function()
+{
+ if (!this.IsUpgrading())
+ return;
+
+ var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+ if (cmpPlayer)
+ {
+ var costs = this.GetResourceCosts(this.upgrading);
+ cmpPlayer.AddResources(costs);
+ }
+
+ this.ChangeUpgradedEntityCount(-1);
+
+ this.upgrading = false;
+ this.CancelTimer();
+ this.SetElapsedTime(0);
+};
+
+Upgrade.prototype.GetUpgradeTime = function(templateArg)
+{
+ var template = this.upgrading || templateArg;
+ var choice = this.upgradeTemplates[template];
+ if (!choice)
+ return undefined;
+ return this.template[choice].Time ? ApplyValueModificationsToEntity("Upgrade/Time", +this.template[choice].Time, this.entity) : 0;
+};
+
+Upgrade.prototype.GetElapsedTime = function()
+{
+ return this.elapsedTime;
+};
+
+Upgrade.prototype.GetProgress = function()
+{
+ if (!this.IsUpgrading())
+ return undefined;
+ return this.GetUpgradeTime() == 0 ? 1 : this.elapsedTime / this.GetUpgradeTime();
+};
+
+Upgrade.prototype.SetElapsedTime = function(time)
+{
+ this.elapsedTime = time;
+};
+
+Upgrade.prototype.UpgradeProgress = function(data, lateness)
+{
+ if (this.elapsedTime < this.GetUpgradeTime())
+ {
+ this.SetElapsedTime(this.GetElapsedTime() + UPGRADING_PROGRESS_INTERVAL + lateness);
+ return;
+ }
+
+ this.CancelTimer();
+
+ var newEntity = ChangeEntityTemplate(this.entity, this.upgrading);
+
+ if (newEntity)
+ PlaySound("upgraded", newEntity);
+};
+
+Engine.RegisterComponentType(IID_Upgrade, "Upgrade", Upgrade);
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js (revision 18426)
+++ binaries/data/mods/public/simulation/helpers/Commands.js (working copy)
@@ -580,12 +580,6 @@
}
},
- "wall-to-gate": function(player, cmd, data)
- {
- for (let ent of data.entities)
- TryTransformWallToGate(ent, data.cmpPlayer, cmd);
- },
-
"lock-gate": function(player, cmd, data)
{
for (let ent of data.entities)
@@ -660,6 +654,69 @@
}
},
+ "upgrade": function(player, cmd, data)
+ {
+ for (let ent of data.entities)
+ {
+ var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
+
+ if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template))
+ continue;
+
+ if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template))
+ {
+ var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ cmpGUIInterface.PushNotification({
+ "players": [data.cmpPlayer.GetPlayerID()],
+ "message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.")
+ });
+ continue;
+ }
+
+ if (!CanGarrisonedChangeTemplate(ent, cmd.template))
+ {
+ var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ cmpGUIInterface.PushNotification({
+ "players": [data.cmpPlayer.GetPlayerID()],
+ "message": markForTranslation("Cannot upgrade a garrisoned entity.")
+ });
+ continue;
+ }
+
+ // Check entity limits
+ var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ var template = cmpTemplateManager.GetTemplate(cmd.template);
+ var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
+ if (template.TrainingRestrictions && !cmpEntityLimits.AllowedToTrain(template.TrainingRestrictions.Category, 1) ||
+ template.BuildRestrictions && !cmpEntityLimits.AllowedToBuild(template.BuildRestrictions.Category))
+ {
+ if (g_DebugCommands)
+ warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd));
+ continue;
+ }
+
+ var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
+ if (cmpUpgrade.GetRequiredTechnology(cmd.template) && !cmpTechnologyManager.IsTechnologyResearched(cmpUpgrade.GetRequiredTechnology(cmd.template)))
+ {
+ if (g_DebugCommands)
+ warn("Invalid command: upgrading requires unresearched technology: " + uneval(cmd));
+ continue;
+ }
+
+ cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer);
+ }
+ },
+
+ "cancel-upgrade": function(player, cmd, data)
+ {
+ for (let ent of data.entities)
+ {
+ let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
+ if (cmpUpgrade)
+ cmpUpgrade.CancelUpgrade(data.cmpPlayer);
+ }
+ },
+
"attack-request": function(player, cmd, data)
{
// Send a chat message to human players
@@ -1557,78 +1614,6 @@
return entities.filter(ent => CanControlUnitOrIsAlly(ent, player, controlAll));
}
-/**
- * Try to transform a wall to a gate
- */
-function TryTransformWallToGate(ent, cmpPlayer, cmd)
-{
- var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
- if (!cmpIdentity)
- return;
-
- if (!cmpIdentity.HasClass("LongWall"))
- {
- if (g_DebugCommands)
- warn("Invalid command: invalid wall conversion to gate for player: " + uneval(cmd));
- return;
- }
-
- var gate = Engine.AddEntity(cmd.template);
-
- var cmpCost = Engine.QueryInterface(gate, IID_Cost);
- if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
- {
- if (g_DebugCommands)
- warn("Invalid command: convert gate cost check failed: " + 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);
Index: binaries/data/mods/public/simulation/helpers/Transform.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Transform.js (nonexistent)
+++ binaries/data/mods/public/simulation/helpers/Transform.js (working copy)
@@ -0,0 +1,206 @@
+// Helper functions to change an entity's template and check if the transformation is possible
+
+// returns the ID of the new entity or INVALID_ENTITY.
+function ChangeEntityTemplate(oldEnt, newTemplate)
+{
+ // Done un/packing, copy our parameters to the final entity
+ var newEnt = Engine.AddEntity(newTemplate);
+ if (newEnt == INVALID_ENTITY)
+ {
+ error("Transform.js: Error replacing entity " + oldEnt + " for a '" + newTemplate + "'");
+ return INVALID_ENTITY;
+ }
+
+ var cmpPosition = Engine.QueryInterface(oldEnt, IID_Position);
+ var cmpNewPosition = Engine.QueryInterface(newEnt, IID_Position);
+ if (cmpPosition && cmpNewPosition)
+ {
+ if (cmpPosition.IsInWorld())
+ {
+ var pos = cmpPosition.GetPosition2D();
+ cmpNewPosition.JumpTo(pos.x, pos.y);
+ }
+ var rot = cmpPosition.GetRotation();
+ cmpNewPosition.SetYRotation(rot.y);
+ cmpNewPosition.SetXZRotation(rot.x, rot.z);
+ cmpNewPosition.SetHeightOffset(cmpPosition.GetHeightOffset());
+ }
+
+ var cmpOwnership = Engine.QueryInterface(oldEnt, IID_Ownership);
+ var cmpNewOwnership = Engine.QueryInterface(newEnt, IID_Ownership);
+ if (cmpOwnership && cmpNewOwnership)
+ cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
+
+ // Copy control groups
+ var cmpObstruction = Engine.QueryInterface(oldEnt, IID_Obstruction);
+ var cmpNewObstruction = Engine.QueryInterface(newEnt, IID_Obstruction);
+ if (cmpObstruction && cmpNewObstruction)
+ {
+ cmpNewObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
+ cmpNewObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
+ }
+
+ // Rescale capture points
+ var cmpCapturable = Engine.QueryInterface(oldEnt, IID_Capturable);
+ var cmpNewCapturable = Engine.QueryInterface(newEnt, IID_Capturable);
+ if (cmpCapturable && cmpNewCapturable)
+ {
+ let scale = cmpCapturable.GetMaxCapturePoints() / cmpNewCapturable.GetMaxCapturePoints();
+ let newCp = cmpCapturable.GetCapturePoints().map(v => v / scale);
+ cmpNewCapturable.SetCapturePoints(newCp);
+ }
+
+ // Maintain current health level
+ var cmpHealth = Engine.QueryInterface(oldEnt, IID_Health);
+ var cmpNewHealth = Engine.QueryInterface(newEnt, IID_Health);
+ if (cmpHealth && cmpNewHealth)
+ {
+ var healthLevel = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
+ cmpNewHealth.SetHitpoints(Math.round(cmpNewHealth.GetMaxHitpoints() * healthLevel));
+ }
+
+ var cmpUnitAI = Engine.QueryInterface(oldEnt, IID_UnitAI);
+ var cmpNewUnitAI = Engine.QueryInterface(newEnt, IID_UnitAI);
+ if (cmpUnitAI && cmpNewUnitAI)
+ {
+ var pos = cmpUnitAI.GetHeldPosition();
+ if (pos)
+ cmpNewUnitAI.SetHeldPosition(pos.x, pos.z);
+ if (cmpUnitAI.GetStanceName())
+ cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName());
+ cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders());
+ cmpNewUnitAI.SetGuardOf(cmpUnitAI.IsGuardOf());
+ }
+
+ // Maintain the list of guards
+ var cmpGuard = Engine.QueryInterface(oldEnt, IID_Guard);
+ var cmpNewGuard = Engine.QueryInterface(newEnt, IID_Guard);
+ if (cmpGuard && cmpNewGuard)
+ cmpNewGuard.SetEntities(cmpGuard.GetEntities());
+
+ TransferGarrisonedUnits(oldEnt, newEnt);
+
+ Engine.BroadcastMessage(MT_EntityRenamed, { "entity": oldEnt, "newentity": newEnt });
+
+ Engine.DestroyEntity(oldEnt);
+
+ return newEnt;
+};
+
+function CanGarrisonedChangeTemplate(ent, template)
+{
+ var cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ var unitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ if (cmpPosition && !cmpPosition.IsInWorld() && unitAI && unitAI.IsGarrisoned())
+ {
+ // We're a garrisoned unit, assume impossibility as I've been unable to find a way to get the holder ID.
+ // TODO: change this if that ever becomes possibles
+ return false;
+ }
+ return true;
+}
+
+function ObstructionsBlockingTemplateChange(ent, templateArg)
+{
+ var previewEntity = Engine.AddEntity("preview|"+templateArg);
+
+ if (previewEntity == INVALID_ENTITY)
+ return true;
+
+ var cmpBuildRestrictions = Engine.QueryInterface(previewEntity, IID_BuildRestrictions);
+ var cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+
+ var cmpNewPosition = Engine.QueryInterface(previewEntity, IID_Position);
+
+ // Return false if no ownership as BuildRestrictions.CheckPlacement needs an owner and I have no idea if false or true is better
+ // Plus there are no real entities without owners currently.
+ if (!cmpBuildRestrictions || !cmpPosition || !cmpOwnership)
+ return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, false);
+
+ var pos = cmpPosition.GetPosition2D();
+ var angle = cmpPosition.GetRotation();
+ // move us away to prevent our own obstruction from blocking the upgrade.
+ cmpPosition.MoveOutOfWorld();
+
+ cmpNewPosition.JumpTo(pos.x, pos.y);
+ cmpNewPosition.SetYRotation(angle.y);
+
+ var cmpNewOwnership = Engine.QueryInterface(previewEntity, IID_Ownership);
+ cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
+
+ var checkPlacement = cmpBuildRestrictions.CheckPlacement();
+
+ if (checkPlacement && !checkPlacement.success)
+ return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, true);
+
+ var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ var template = cmpTemplateManager.GetTemplate(cmpTemplateManager.GetCurrentTemplateName(ent));
+ var newTemplate = cmpTemplateManager.GetTemplate(templateArg);
+
+ // Check if units are blocking our template change
+ if (template.Obstruction && newTemplate.Obstruction)
+ {
+ // This only needs to be done if the new template is strictly bigger than the old one
+ // "Obstructions" are annoying to test so just check.
+ if (newTemplate.Obstruction.Obstructions ||
+
+ newTemplate.Obstruction.Static && template.Obstruction.Static &&
+ (newTemplate.Obstruction.Static["@width"] > template.Obstruction.Static["@width"] ||
+ newTemplate.Obstruction.Static["@depth"] > template.Obstruction.Static["@depth"]) ||
+ newTemplate.Obstruction.Static && template.Obstruction.Unit &&
+ (newTemplate.Obstruction.Static["@width"] > template.Obstruction.Unit["@radius"] ||
+ newTemplate.Obstruction.Static["@depth"] > template.Obstruction.Unit["@radius"]) ||
+
+ newTemplate.Obstruction.Unit && template.Obstruction.Unit &&
+ newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Unit["@radius"] ||
+ newTemplate.Obstruction.Unit && template.Obstruction.Static &&
+ (newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Static["@width"] ||
+ newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Static["@depth"]))
+ {
+ var cmpNewObstruction = Engine.QueryInterface(previewEntity, IID_Obstruction);
+ if (cmpNewObstruction && cmpNewObstruction.GetBlockMovementFlag())
+ {
+ // Check for units
+ var collisions = cmpNewObstruction.GetEntityCollisions(false, true);
+ if (collisions.length)
+ return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, true);
+ }
+ }
+ }
+
+ return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, false);
+};
+
+function DeleteEntityAndReturn(ent, cmpPosition, position, angle, cmpNewPosition, ret)
+{
+ // prevent preview from interfering in the world
+ cmpNewPosition.MoveOutOfWorld();
+ cmpPosition.JumpTo(position.x, position.y);
+ cmpPosition.SetYRotation(angle.y);
+
+ Engine.DestroyEntity(ent);
+ return ret;
+};
+
+function TransferGarrisonedUnits(oldEnt, newEnt)
+{
+ // Transfer garrisoned units if possible, or unload them
+ var cmpGarrison = Engine.QueryInterface(oldEnt, IID_GarrisonHolder);
+ var cmpNewGarrison = Engine.QueryInterface(newEnt, IID_GarrisonHolder);
+ if (!cmpNewGarrison || !cmpGarrison || !cmpGarrison.GetEntities().length)
+ return; // nothing to do as the code will by default unload all.
+
+ var garrisonedEntities = cmpGarrison.GetEntities().slice();
+ for (let j in garrisonedEntities)
+ {
+ var cmpUnitAI = Engine.QueryInterface(garrisonedEntities[j], IID_UnitAI);
+ cmpGarrison.Eject(garrisonedEntities[j]);
+ cmpUnitAI.Autogarrison(newEnt);
+ cmpNewGarrison.Garrison(garrisonedEntities[j]);
+ }
+};
+
+Engine.RegisterGlobal("ChangeEntityTemplate", ChangeEntityTemplate);
+Engine.RegisterGlobal("CanGarrisonedChangeTemplate", CanGarrisonedChangeTemplate);
+Engine.RegisterGlobal("ObstructionsBlockingTemplateChange", ObstructionsBlockingTemplateChange);
Index: binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml (revision 18426)
+++ binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml (working copy)
@@ -26,7 +26,6 @@
other/wallset_palisade
Palisade
Wooden Wall
- Convert Wooden Wall into Wooden Gate
-StoneWall Palisade
gaia/special_palisade.png
phase_village
@@ -45,4 +44,14 @@
11.0
+
+
+ other/palisades_rocks_gate
+
+ 0
+ 20
+
+
+
+
Index: binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (revision 18426)
+++ binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (working copy)
@@ -55,7 +55,6 @@
structures/palisade_wall.png
A wooden and turf palisade buildable in enemy and neutral territories.
- Convert Siege Wall into Siege Wall Gate
Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia.
Index: binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml (revision 18426)
+++ binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml (working copy)
@@ -38,6 +38,15 @@
LongWall
Long wall segments can be converted to gates.
- Convert Stone Wall into City Gate
+
+
+ structures/{civ}_wall_gate
+ This will allow you to let units circulate through your fortifications.
+
+ 60
+
+
+
+