1
0
forked from mirrors/0ad

# Construct buildings from foundations.

Display construction costs in tooltips.
Stop buildings leaving corpses.
Add debug info option to GUI.

This was SVN commit r7352.
This commit is contained in:
Ykkrosh
2010-03-12 21:41:40 +00:00
parent c55cba34e5
commit 9213ee8c27
17 changed files with 505 additions and 34 deletions
@@ -81,10 +81,15 @@ function determineAction(x, y)
if (entState.attack && targetState.player != entState.player)
return {"type": "attack", "cursor": "action-attack", "target": targets[0]};
// Resource -> gather
var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply);
if (resource)
return {"type": "gather", "cursor": "action-gather-"+resource, "target": targets[0]};
// If a builder, then: Foundation -> build
if (entState.buildEntities && targetState.foundation)
return {"type": "build", "cursor": "action-build", "target": targets[0]};
// TODO: need more actions
// If we don't do anything more specific, just walk
@@ -233,9 +238,17 @@ function handleInputAfterGui(ev)
Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target});
return true;
case "build": // (same command as repair)
case "repair":
Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target});
return true;
case "gather":
Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target});
return true;
default:
throw new Error("Invalid action.type "+action.type);
}
}
break;
@@ -288,17 +301,32 @@ function handleInputAfterGui(ev)
case "mousemotion":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
var angle = Math.PI;
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": placementEntity, "x": target.x, "z": target.z, "angle": angle});
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementEntity,
"x": target.x,
"z": target.z,
"angle": angle
});
return false; // continue processing mouse motion
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
var selection = g_Selection.toList();
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
var angle = Math.PI;
// Remove the preview
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
Engine.PostNetworkCommand({"type": "construct", "template": placementEntity, "x": target.x, "z": target.z, "angle": angle});
// Start the construction
Engine.PostNetworkCommand({
"type": "construct",
"template": placementEntity,
"x": target.x,
"z": target.z,
"angle": angle,
"entities": selection
});
inputState = INPUT_NORMAL;
return true;
@@ -1,3 +1,4 @@
// Cache dev-mode settings that are frequently or widely used
var g_DevSettings = {
controlAll: false
};
@@ -36,7 +37,7 @@ function onSimulationUpdate()
if (!simState)
return;
// updateDebug(simState);
updateDebug(simState);
updatePlayerDisplay(simState);
@@ -46,14 +47,28 @@ function onSimulationUpdate()
function updateDebug(simState)
{
var debug = getGUIObjectByName("debug");
if (getGUIObjectByName("devDisplayState").checked)
{
debug.hidden = false;
}
else
{
debug.hidden = true;
return;
}
var text = uneval(simState);
var selection = g_Selection.toList();
if (selection.length)
{
var entState = Engine.GuiInterfaceCall("GetEntityState", selection[0]);
var template = Engine.GuiInterfaceCall("GetTemplateData", entState.template);
text += "\n\n" + uneval(entState) + "\n\n" + uneval(template);
if (entState)
{
var template = Engine.GuiInterfaceCall("GetTemplateData", entState.template);
text += "\n\n" + uneval(entState) + "\n\n" + uneval(template);
}
}
debug.caption = text;
@@ -168,8 +183,21 @@ function updateUnitDisplay()
else
name = template.name.specific || template.name.generic || "???";
var tooltip = "[font=trebuchet14b]" + name + "[/font]";
if (template.cost)
{
var costs = [];
if (template.cost.food) costs.push("[font=tahoma10b]Food:[/font] " + template.cost.food);
if (template.cost.wood) costs.push("[font=tahoma10b]Wood:[/font] " + template.cost.wood);
if (template.cost.metal) costs.push("[font=tahoma10b]Metal:[/font] " + template.cost.metal);
if (template.cost.stone) costs.push("[font=tahoma10b]Stone:[/font] " + template.cost.stone);
if (costs.length)
tooltip += "\n" + costs.join(", ");
}
button.hidden = false;
button.tooltip = "Construct " + name;
button.tooltip = tooltip;
button.onpress = (function(b) { return function() { testBuild(b) } })(build);
// (need nested functions to get the closure right)
@@ -41,6 +41,9 @@
<object size="100%-256 32 100%-16 128">
<object size="0 0 100%-18 16" type="text" text_align="right">Control all units</object>
<object size="100%-16 0 100% 16" type="checkbox" name="devControlAll" style="wheatCrossBox"/>
<object size="0 16 100%-18 32" type="text" text_align="right">Display selection state</object>
<object size="100%-16 16 100% 32" type="checkbox" name="devDisplayState" style="wheatCrossBox"/>
</object>
<!-- Debug text -->
@@ -16,4 +16,33 @@ Builder.prototype.GetEntitiesList = function()
return string.split(/\s+/);
};
Builder.prototype.GetRange = function()
{
return { "max": 16, "min": 0 };
// maybe this should depend on the unit or target or something?
}
/**
* Build/repair the target entity. This should only be called after a successful range check.
* It should be called at a rate of once per second.
*/
Builder.prototype.PerformBuilding = function(target)
{
var rate = 0.1; // XXX
// If it's a foundation, then build it
var cmpFoundation = Engine.QueryInterface(target, IID_Foundation);
if (cmpFoundation)
{
var finished = cmpFoundation.Build(this.entity, rate);
return { "finished": finished };
}
else
{
// TODO: do some kind of repairing
return { "finished": true };
}
};
Engine.RegisterComponentType(IID_Builder, "Builder", Builder);
@@ -0,0 +1,114 @@
function Foundation() {}
Foundation.prototype.Init = function()
{
this.buildProgress = 0.0; // 0 <= progress <= 1
};
Foundation.prototype.InitialiseConstruction = function(owner, template)
{
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
this.finalTemplateName = template;
this.addedHitpoints = cmpHealth.GetHitpoints();
this.maxHitpoints = cmpHealth.GetMaxHitpoints();
// We need to know the owner in OnDestroy, but at that point the entity has already been
// decoupled from its owner, so we need to remember it in here (and assume it won't change)
this.owner = owner;
};
Foundation.prototype.GetBuildPercentage = function()
{
return Math.floor(this.buildProgress * 100);
};
Foundation.prototype.OnDestroy = function()
{
// Refund a portion of the construction cost, proportional to the amount of build progress remaining
if (this.buildProgress == 1.0)
return;
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(this.owner), IID_Player);
var cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
var costs = cmpCost.GetResourceCosts();
for (var r in costs)
{
var scaled = Math.floor(costs[r] * (1.0 - this.buildProgress));
if (scaled)
cmpPlayer.AddResource(r, scaled);
}
};
/**
* Perform some amount of construction work.
* Returns true if the construction is completed.
*/
Foundation.prototype.Build = function(builderEnt, work)
{
// Do nothing if we've already finished building
if (this.buildProgress == 1.0)
return true;
var amount = work; // TODO: adjust by time cost of this building
// TODO: implement some kind of diminishing returns for multiple builders.
// e.g. record the set of entities that build this, then every ~2 seconds
// count them (and reset the list), and apply some function to the count to get
// a factor, and apply that factor here.
this.buildProgress += amount;
if (this.buildProgress > 1.0)
this.buildProgress = 1.0;
// Add an appropriate proportion of hitpoints
var targetHP = Math.max(0, Math.min(this.maxHitpoints, Math.floor(this.maxHitpoints * this.buildProgress)));
var deltaHP = targetHP - this.addedHitpoints;
if (deltaHP > 0)
{
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
cmpHealth.Increase(deltaHP);
this.addedHitpoints += deltaHP;
}
if (this.buildProgress >= 1.0)
{
// Finished construction
// Create the real entity
var building = Engine.AddEntity(this.finalTemplateName);
// Copy various parameters from the foundation
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position);
var pos = cmpPosition.GetPosition();
cmpBuildingPosition.JumpTo(pos.x, pos.z);
var rot = cmpPosition.GetRotation();
cmpBuildingPosition.SetYRotation(rot.y);
cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
// TODO: should add a ICmpPosition::CopyFrom() instead of all this
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
cmpBuildingHealth.SetHitpoints(cmpHealth.GetHitpoints());
Engine.DestroyEntity(this.entity);
return true;
}
else
{
return false;
}
};
Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation);
@@ -11,21 +11,21 @@ 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 cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
var player = {
var playerData = {
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"resourceCounts": cmpPlayer.GetResourceCounts()
};
ret.players.push(player);
ret.players.push(playerData);
}
return ret;
};
@@ -73,6 +73,14 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
ret.buildEntities = cmpBuilder.GetEntitiesList();
}
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
{
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage()
};
}
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
{
@@ -114,6 +122,18 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
ret.icon_cell = template.Identity.IconCell;
}
if (template.Cost)
{
ret.cost = {};
if (template.Cost.Resources)
{
if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food;
if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
}
}
return ret;
};
@@ -2,7 +2,9 @@ function Health() {}
Health.prototype.Init = function()
{
this.hitpoints = this.GetMaxHitpoints();
// Default to <Initial>, but use <Max> if it's undefined or zero
// (Allowing 0 initial HP would break our death detection code)
this.hitpoints = +(this.template.Initial || this.GetMaxHitpoints());
};
//// Interface functions ////
@@ -17,6 +19,15 @@ Health.prototype.GetMaxHitpoints = function()
return +this.template.Max;
};
Health.prototype.SetHitpoints = function(value)
{
// If we're already dead, don't allow resurrection
if (this.hitpoints == 0)
return;
this.hitpoints = Math.max(1, Math.min(this.GetMaxHitpoints(), value));
}
Health.prototype.Reduce = function(amount)
{
if (amount >= this.hitpoints)
@@ -26,7 +37,9 @@ Health.prototype.Reduce = function(amount)
// might get called multiple times)
if (this.hitpoints)
{
this.CreateCorpse();
if (this.template.DeathType == "corpse")
this.CreateCorpse();
Engine.DestroyEntity(this.entity);
}
@@ -38,6 +51,15 @@ Health.prototype.Reduce = function(amount)
}
}
Health.prototype.Increase = function(amount)
{
// If we're already dead, don't allow resurrection
if (this.hitpoints == 0)
return;
this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
}
//// Private functions ////
Health.prototype.CreateCorpse = function()
@@ -8,10 +8,10 @@ Player.prototype.Init = function()
this.popCount = 0;
this.popLimit = 50;
this.resourceCount = {
"food": 100,
"wood": 50,
"metal": 0,
"stone": 0
"food": 2000,
"wood": 1500,
"metal": 500,
"stone": 1000
};
};
@@ -35,11 +35,25 @@ Player.prototype.GetResourceCounts = function()
return this.resourceCount;
};
Player.prototype.AddResources = function(type, amount)
Player.prototype.AddResource = function(type, amount)
{
this.resourceCount[type] += (+amount);
};
Player.prototype.TrySubtractResources = function(amounts)
{
// Check we can afford it all
for (var type in amounts)
if (amounts[type] > this.resourceCount[type])
return false;
// Subtract the resources
for (var type in amounts)
this.resourceCount[type] -= amounts[type];
return true;
};
Player.prototype.OnGlobalOwnershipChanged = function(msg)
{
if (msg.from == this.playerID)
@@ -40,7 +40,7 @@ ResourceGatherer.prototype.PerformGather = function(target)
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(cmpOwnership.GetOwner()), IID_Player);
cmpPlayer.AddResources(type.generic, status.amount);
cmpPlayer.AddResource(type.generic, status.amount);
return status;
};
@@ -8,7 +8,8 @@ and then autonomously carry out the orders. It might need to be entirely redesig
const STATE_IDLE = 0;
const STATE_WALKING = 1;
const STATE_ATTACKING = 2;
const STATE_GATHERING = 3;
const STATE_REPAIRING = 3;
const STATE_GATHERING = 4;
/* Attack process:
* When starting attack:
@@ -26,9 +27,9 @@ const STATE_GATHERING = 3;
* faster-than-normal attacks)
*/
/* Gather process is about the same, except with less synchronisation - the action
/* Repeat/Gather process is about the same, except with less synchronisation - the action
* is just performed 1sec after initiated, and then repeated every 1sec.
* (TODO: it'd be nice to avoid most of the duplication between Attack and Gather code)
* (TODO: it'd be nice to avoid most of the duplication between Attack and Repeat and Gather code)
*/
function UnitAI() {}
@@ -45,6 +46,11 @@ UnitAI.prototype.Init = function()
// Current target entity ID
this.attackTarget = undefined;
// Timer for RepairTimeout
this.repairTimer = undefined;
// Current target entity ID
this.repairTarget = undefined;
// Timer for GatherTimeout
this.gatherTimer = undefined;
// Current target entity ID
@@ -84,6 +90,24 @@ UnitAI.prototype.Attack = function(target)
this.state = STATE_ATTACKING;
};
UnitAI.prototype.Repair = function(target)
{
// Verify that we're able to respond to Repair commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
return;
// TODO: verify that this is a valid target
// Stop any previous action timers
this.CancelTimers();
// Remember the target, and start moving towards it
this.repairTarget = target;
this.MoveToTarget(target, cmpBuilder.GetRange());
this.state = STATE_REPAIRING;
};
UnitAI.prototype.Gather = function(target)
{
// Verify that we're able to respond to Gather commands
@@ -147,6 +171,22 @@ UnitAI.prototype.OnMotionChanged = function(msg)
// Start the idle animation before we switch to the attack
this.SelectAnimation("idle");
}
else if (this.state == STATE_REPAIRING)
{
// We were repairing, and have stopped moving
// => check if we can still reach the target now
if (!this.MoveIntoRepairRange())
return;
// In range, so perform the repairing
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, {});
// Start the repair/build animation
this.SelectAnimation("build");
}
else if (this.state == STATE_GATHERING)
{
// We were gathering, and have stopped moving
@@ -211,6 +251,13 @@ UnitAI.prototype.CancelTimers = function()
this.attackTimer = undefined;
}
if (this.repairTimer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.repairTimer);
this.repairTimer = undefined;
}
if (this.gatherTimer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
@@ -249,6 +296,32 @@ UnitAI.prototype.MoveIntoAttackRange = function()
return true;
};
UnitAI.prototype.MoveIntoRepairRange = function()
{
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
var range = cmpBuilder.GetRange();
var rangeStatus = this.CheckRange(this.repairTarget, range);
if (rangeStatus.error)
{
if (rangeStatus.error == "out-of-range")
{
// Out of range => need to move closer
// (The target has probably moved while we were chasing it)
this.MoveToTarget(this.repairTarget, range);
return false;
}
// Otherwise it's impossible to reach the target, so give up
// and switch back to idle
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return false;
}
return true;
};
UnitAI.prototype.MoveIntoGatherRange = function()
{
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
@@ -275,6 +348,8 @@ UnitAI.prototype.MoveIntoGatherRange = function()
return true;
};
// TODO: refactor all this repetitive code
UnitAI.prototype.SelectAnimation = function(name, once, speed)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
@@ -302,12 +377,12 @@ UnitAI.prototype.AttackTimeout = function(data)
if (this.state != STATE_ATTACKING)
return;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Check if we can still reach the target
if (!this.MoveIntoAttackRange())
return;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Play the attack animation
this.SelectAnimation("melee", false, 1);
@@ -323,18 +398,47 @@ UnitAI.prototype.AttackTimeout = function(data)
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", timers.repeat, data);
};
UnitAI.prototype.RepairTimeout = function(data)
{
// If we stopped repairing before this timeout, then don't do any processing here
if (this.state != STATE_REPAIRING)
return;
// Check if we can still reach the target
if (!this.MoveIntoRepairRange())
return;
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
// Repair/build the target
var status = cmpBuilder.PerformBuilding(this.repairTarget);
// If the target is fully built and repaired, then stop and go back to idle
if (status.finished)
{
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return;
}
// Set a timer to gather again
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, data);
};
UnitAI.prototype.GatherTimeout = function(data)
{
// If we stopped gathering before this timeout, then don't do any processing here
if (this.state != STATE_GATHERING)
return;
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Check if we can still reach the target
if (!this.MoveIntoGatherRange())
return;
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Gather from the target
var status = cmpResourceGatherer.PerformGather(this.gatherTarget);
@@ -0,0 +1 @@
Engine.RegisterInterface("Foundation");
@@ -1,6 +1,7 @@
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
@@ -24,6 +24,17 @@ function ProcessCommand(player, cmd)
}
break;
case "repair":
// This covers both repairing damaged buildings, and constructing unfinished foundations
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (!ai)
continue;
ai.Repair(cmd.target);
}
break;
case "gather":
for each (var ent in cmd.entities)
{
@@ -35,17 +46,55 @@ function ProcessCommand(player, cmd)
break;
case "construct":
// TODO: this should do all sorts of stuff with foundations and resource costs etc
var ent = Engine.AddEntity(cmd.template);
if (ent)
/*
* 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.
*/
// Find the player
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Tentatively create the foundation (we don't know yet if the player can really afford it)
var ent = Engine.AddEntity("foundation|" + cmd.template);
// TODO: report errors (e.g. invalid template names)
var cmpCost = Engine.QueryInterface(ent, IID_Cost);
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
var pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
// TODO: report error to player (they ran out of resources)
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
break;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(cmd.angle);
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// Tell the units to start building this new entity
ProcessCommand(player, {
"type": "repair",
"entities": cmd.entities,
"target": ent
});
break;
default:
@@ -9,6 +9,7 @@
<Population>1</Population>
</Cost>
<Health>
<DeathType>corpse</DeathType>
<Max>100</Max>
</Health>
<Armour>
@@ -127,6 +127,10 @@ private:
// Copy the non-interactive components of an entity template (position, actor, etc) into
// a new entity template
void CopyPreviewSubset(CParamNode& out, const CParamNode& in);
// Copy the components of an entity necessary for a construction foundation
// (position, actor, armour, health, etc) into a new entity template
void CopyFoundationSubset(CParamNode& out, const CParamNode& in);
};
REGISTER_COMPONENT_TYPE(TemplateManager)
@@ -216,6 +220,21 @@ bool CCmpTemplateManager::LoadTemplateFile(const std::wstring& templateName, int
return true;
}
// Handle special case "foundation|foo"
if (templateName.find(L"foundation|") == 0)
{
// Load the base entity template, if it wasn't already loaded
std::wstring baseName = templateName.substr(11);
if (!LoadTemplateFile(baseName, depth+1))
{
LOGERROR(L"Failed to load entity template '%ls'", baseName.c_str());
return NULL;
}
// Copy a subset to the requested template
CopyFoundationSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
return true;
}
// Normal case: templateName is an XML file:
VfsPath path = VfsPath(TEMPLATE_ROOT) / (templateName + L".xml");
@@ -339,3 +358,36 @@ void CCmpTemplateManager::CopyPreviewSubset(CParamNode& out, const CParamNode& i
// In the future, we might want to add some extra flags to certain components, to indicate they're
// running in 'preview' mode and should not e.g. register with global managers
}
void CCmpTemplateManager::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<std::string> permittedComponentTypes;
permittedComponentTypes.insert("Ownership");
permittedComponentTypes.insert("Position");
permittedComponentTypes.insert("Identity");
permittedComponentTypes.insert("Obstruction");
permittedComponentTypes.insert("Selectable");
permittedComponentTypes.insert("Footprint");
permittedComponentTypes.insert("Armour");
permittedComponentTypes.insert("Health");
permittedComponentTypes.insert("Cost");
CParamNode::LoadXMLString(out, "<Entity/>");
out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
// TODO: the foundation shouldn't be considered an obstruction by default, until construction has
// really started, to prevent players abusing it to block their opponents
// TODO: Use the appropriate actor
CParamNode::LoadXMLString(out, "<Entity><VisualActor><Actor>structures/fndn_4x4.xml</Actor></VisualActor></Entity>");
// Add the Foundation component, to deal with the construction process
CParamNode::LoadXMLString(out, "<Entity><Foundation/></Entity>");
// Initialise health to 1
CParamNode::LoadXMLString(out, "<Entity><Health><Initial>1</Initial></Health></Entity>");
}
@@ -43,6 +43,10 @@ public:
* based on entity template "foo" with the non-graphical components removed.
* (This is for previewing construction/placement of units.)
*
* If templateName is of the form "foundation|foo" then it will load a template
* based on entity template "foo" with various components removed and a few changed
* and added. (This is for constructing foundations of buildings.)
*
* @return NULL on error
*/
virtual const CParamNode* LoadTemplate(entity_id_t ent, const std::wstring& templateName, int playerID) = 0;
+1
View File
@@ -129,6 +129,7 @@ sub convert {
if ($data->{Traits}[0]{Health}) {
$out .= qq{$i<Health>\n};
$out .= qq{$i$i<DeathType>corpse</DeathType>\n} if $name eq 'template_unit';
$out .= qq{$i$i<Max>$data->{Traits}[0]{Health}[0]{Max}[0]</Max>\n} if $data->{Traits}[0]{Health}[0]{Max};
$out .= qq{$i$i<RegenRate>$data->{Traits}[0]{Health}[0]{RegenRate}[0]</RegenRate>\n} if $data->{Traits}[0]{Health}[0]{RegenRate};
$out .= qq{$i</Health>\n};