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:
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user