Allow entities to upgrade into other entities.

This new components allows giving the upgrade a cost, required
technologies, and a required time.
Implement gates using this generic component.
Fixes #2706

This was SVN commit r18467.
This commit is contained in:
wraitii 2016-07-01 19:43:26 +00:00
parent 1276b98965
commit b2f4b0f494
18 changed files with 828 additions and 245 deletions

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0491f1ad9ca0c77cf2a4c187cc3883155d3a2b49d72fc7591c98c74e86763fb
size 2938

View file

@ -277,7 +277,6 @@ function GetTemplateDataHelper(template, player, auraTemplates)
};
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);
}

View file

@ -291,7 +291,8 @@ function getEntityCostComponentsTooltipString(template, trainNum, entity)
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 = [];

View file

@ -1632,27 +1632,25 @@ function cancelPackUnit(pack)
});
}
// 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

View file

@ -472,35 +472,11 @@ g_SelectionPanels.Gate = {
},
"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])
{
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)
if (state.gate && !gates.length)
{
gates.push({
"gate": state.gate,
@ -521,54 +497,25 @@ g_SelectionPanels.Gate = {
delete gates[j].gate.locked;
}
// Place wall conversion options after gate lock/unlock icons.
return gates.concat(walls);
return gates;
},
"setupButton": function(data)
{
data.button.onPress = function() {data.item.callback(data.item); };
let tooltips = [data.item.tooltip];
if (data.item.template)
{
data.template = GetTemplateData(data.item.template);
data.wallCount = data.selection.reduce(count, ent => {
let state = GetEntityState(ent);
if (hasClass(state, "LongWall") && !state.gate)
++count;
return count;
}, 0);
tooltips.push(getEntityCostTooltip(data.template, data.wallCount));
data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(data.template, data.wallCount)
});
tooltips.push(getNeededResourcesTooltip(data.neededResources));
}
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
data.button.tooltip = data.item.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;
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;
@ -609,13 +556,33 @@ g_SelectionPanels.Pack = {
}
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)
@ -1052,6 +1019,128 @@ g_SelectionPanels.Training = {
}
};
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)
{
items.push({
"entity": upgrade.entity,
"cost": upgrade.cost,
"time": upgrade.time,
"icon": upgrade.icon,
"tooltip": upgrade.tooltip,
"requiredTechnology": upgrade.requiredTechnology,
});
}
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)
{
if (data.item.tooltip)
tooltip = sprintf(translate("Upgrade into a %(name)s. %(tooltip)s"), {
"name": template.name.generic,
"tooltip": data.item.tooltip
});
else
tooltip = sprintf(translate("Upgrade into a %(name)s."), {"name": template.name.generic});
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)
progressOverlay.size.top = progressOverlay.size.left + Math.round(progress * (progressOverlay.size.right - progressOverlay.size.left));
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.
@ -1069,6 +1158,7 @@ let g_PanelsOrder = [
// 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

View file

@ -4,6 +4,13 @@ const BARTER_RESOURCES = ["food", "wood", "stone", "metal"];
const BARTER_ACTIONS = ["Sell", "Buy"];
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
var g_BarterSell = "food";
function canMoveSelectionIntoFormation(formationTemplate)

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="unitUpgradePanel"
size="10 12 100% 100%"
>
<object size="0 0 100% 100%">
<repeat count="8">
<object name="unitUpgradeButton[n]" hidden="true" style="iconButton" type="button" size="0 0 46 46" tooltip_style="sessionToolTipBottom">
<object name="unitUpgradeIcon[n]" type="image" ghost="true" size="3 3 43 43"/>
<object name="unitUpgradeUpgradeIcon[n]" type="image" ghost="true" size="3 3 43 43" sprite="stretched:session/icons/upgrade.png"/>
<object name="unitUpgradeProgressSlider[n]" type="image" sprite="queueProgressSlider" ghost="true" size="3 3 43 43" z="20"/>
<object name="unitUpgradeSelection[n]" hidden="true" type="image" ghost="true" size="3 3 43 43" sprite="stretched:session/icons/corners.png"/>
</object>
</repeat>
</object>
</object>

View file

@ -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 @@ function getNumberOfRightPanelButtons()
{
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];

View file

@ -237,6 +237,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"market": null,
"mirage": null,
"pack": null,
"upgrade" : null,
"player": -1,
"position": null,
"production": null,
@ -303,6 +304,14 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"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 = {

View file

@ -39,11 +39,6 @@ Identity.prototype.Schema =
"<text/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='GateConversionTooltip'>" +
"<text/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Rollover'>" +
"<text/>" +

View file

@ -33,7 +33,7 @@ Pack.prototype.CancelTimer = function()
{
if (this.timer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
@ -56,9 +56,9 @@ Pack.prototype.Pack = function()
return;
this.packing = true;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetInterval(this.entity, IID_Pack, "PackProgress", 0, PACKING_INTERVAL, {"packing": true});
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("packing", true, 1.0, "packing");
};
@ -70,9 +70,9 @@ Pack.prototype.Unpack = function()
return;
this.packing = true;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetInterval(this.entity, IID_Pack, "PackProgress", 0, PACKING_INTERVAL, {"packing": false});
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("unpacking", true, 1.0, "unpacking");
};
@ -88,7 +88,7 @@ Pack.prototype.CancelPack = function()
this.SetElapsedTime(0);
// Clear animation
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("idle", false, 1.0, "");
};
@ -116,87 +116,22 @@ Pack.prototype.SetElapsedTime = function(time)
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;
}
let 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());
if (newEntity)
PlaySound(this.packed ? "packed" : "unpacked", newEntity);
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
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);

View file

@ -0,0 +1,306 @@
function Upgrade() {}
const UPGRADING_PROGRESS_INTERVAL = 250;
Upgrade.prototype.Schema =
"<oneOrMore>" +
"<element>" +
"<anyName />" +
"<interleave>" +
"<element name='Entity' a:help='Entity to upgrade to'>" +
"<text/>" +
"</element>" +
"<optional>" +
"<element name='Icon' a:help='Icon to show in the GUI'>" +
"<text/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Tooltip' a:help='This will be added to the tooltip to help the player choose why to upgrade.'>" +
"<text/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Time' a:help='Time required to upgrade this entity, in milliseconds'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Cost' a:help='Resource cost to upgrade this unit'>" +
"<oneOrMore>" +
"<choice>" +
"<element name='food'><data type='nonNegativeInteger'/></element>" +
"<element name='wood'><data type='nonNegativeInteger'/></element>" +
"<element name='stone'><data type='nonNegativeInteger'/></element>" +
"<element name='metal'><data type='nonNegativeInteger'/></element>" +
"</choice>" +
"</oneOrMore>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='RequiredTechnology' a:help='Define what technology is required for this upgrade'>" +
"<choice>" +
"<text/>" +
"<empty/>" +
"</choice>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='CheckPlacementRestrictions' a:help='Upgrading will check for placement restrictions (nb:GUI only)'><empty/></element>" +
"</optional>" +
"</interleave>" +
"</element>" +
"</oneOrMore>";
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;
let cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTempMan.GetTemplate(this.upgrading);
let category;
if (template.TrainingRestrictions)
category = template.TrainingRestrictions.Category;
else if (template.BuildRestrictions)
category = template.BuildRestrictions.Category;
if (!category)
return;
let cmpEntityLimits = QueryPlayerIDInterface(this.owner, IID_EntityLimits);
cmpEntityLimits.ChangeCount(category, amount);
};
Upgrade.prototype.CanUpgradeTo = function(template)
{
return this.upgradeTemplates[template] !== undefined;
};
Upgrade.prototype.GetUpgrades = function()
{
let ret = [];
let 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;
let 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;
let choice = this.upgradeTemplates[template];
if (!this.template[choice].Cost)
return {};
let 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;
let 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)
{
let 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;
let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (cmpPlayer)
{
let costs = this.GetResourceCosts(this.upgrading);
cmpPlayer.AddResources(costs);
}
this.ChangeUpgradedEntityCount(-1);
this.upgrading = false;
this.CancelTimer();
this.SetElapsedTime(0);
};
Upgrade.prototype.GetUpgradeTime = function(templateArg)
{
let template = this.upgrading || templateArg;
let 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();
let newEntity = ChangeEntityTemplate(this.entity, this.upgrading);
if (newEntity)
PlaySound("upgraded", newEntity);
};
Engine.RegisterComponentType(IID_Upgrade, "Upgrade", Upgrade);

View file

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

View file

@ -581,12 +581,6 @@ var g_Commands = {
}
},
"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)
@ -661,6 +655,69 @@ var g_Commands = {
}
},
"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
@ -1558,78 +1615,6 @@ function FilterEntityListWithAllies(entities, player, controlAll)
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);

View file

@ -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);

View file

@ -26,7 +26,6 @@
<SelectionGroupName>other/wallset_palisade</SelectionGroupName>
<SpecificName>Palisade</SpecificName>
<GenericName>Wooden Wall</GenericName>
<GateConversionTooltip>Convert Wooden Wall into Wooden Gate</GateConversionTooltip>
<Classes datatype="tokens">-StoneWall Palisade</Classes>
<Icon>gaia/special_palisade.png</Icon>
<RequiredTechnology>phase_village</RequiredTechnology>
@ -45,4 +44,14 @@
<WallPiece>
<Length>11.0</Length>
</WallPiece>
<Upgrade>
<Gate>
<Entity>other/palisades_rocks_gate</Entity>
<Cost>
<stone>0</stone>
<wood>20</wood>
</Cost>
<Time>5000</Time>
</Gate>
</Upgrade>
</Entity>

View file

@ -55,7 +55,6 @@
</Classes>
<Icon>structures/palisade_wall.png</Icon>
<Tooltip>A wooden and turf palisade buildable in enemy and neutral territories.</Tooltip>
<GateConversionTooltip>Convert Siege Wall into Siege Wall Gate</GateConversionTooltip>
<History>Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia.</History>
</Identity>
<Obstruction>

View file

@ -38,6 +38,15 @@
<Identity>
<Classes datatype="tokens">LongWall</Classes>
<Tooltip>Long wall segments can be converted to gates.</Tooltip>
<GateConversionTooltip>Convert Stone Wall into City Gate</GateConversionTooltip>
</Identity>
<Upgrade>
<Gate>
<Entity>structures/{civ}_wall_gate</Entity>
<Tooltip>This will allow you to let units circulate through your fortifications.</Tooltip>
<Cost>
<stone>60</stone>
</Cost>
<Time>10000</Time>
</Gate>
</Upgrade>
</Entity>