forked from 0ad/0ad
# Support resource gathering in new simulation system
This was SVN commit r7322.
This commit is contained in:
parent
321cc8ae8f
commit
f8aca33a14
@ -21,9 +21,9 @@ function updateCursor()
|
||||
var action = determineAction(mouseX, mouseY);
|
||||
if (action)
|
||||
{
|
||||
if (action.type != "move")
|
||||
if (action.cursor)
|
||||
{
|
||||
Engine.SetCursor("action-" + action.type);
|
||||
Engine.SetCursor(action.cursor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,16 @@ function updateCursor()
|
||||
Engine.SetCursor("arrow-default");
|
||||
}
|
||||
|
||||
function findGatherType(gatherer, supply)
|
||||
{
|
||||
if (!gatherer || !supply)
|
||||
return;
|
||||
if (gatherer[supply.type.generic+"."+supply.type.specific])
|
||||
return supply.type.specific;
|
||||
if (gatherer[supply.type.generic])
|
||||
return supply.type.generic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
|
||||
*/
|
||||
@ -61,7 +71,11 @@ function determineAction(x, y)
|
||||
|
||||
// Different owner -> attack
|
||||
if (entState.attack && targetState.player != player)
|
||||
return {"type": "attack", "target": targets[0]};
|
||||
return {"type": "attack", "cursor": "action-attack", "target": targets[0]};
|
||||
|
||||
var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply);
|
||||
if (resource)
|
||||
return {"type": "gather", "cursor": "action-gather-"+resource, "target": targets[0]};
|
||||
|
||||
// TODO: need more actions
|
||||
|
||||
@ -140,6 +154,10 @@ function handleInputAfterGui(ev)
|
||||
case "attack":
|
||||
Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target});
|
||||
return true;
|
||||
|
||||
case "gather":
|
||||
Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,37 +28,13 @@ Attack.prototype.GetAttackStrengths = function()
|
||||
};
|
||||
};
|
||||
|
||||
function hypot2(x, y)
|
||||
Attack.prototype.GetRange = function()
|
||||
{
|
||||
return x*x + y*y;
|
||||
}
|
||||
|
||||
Attack.prototype.CheckRange = function(target)
|
||||
{
|
||||
// Target must be in the world
|
||||
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
|
||||
if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld())
|
||||
return { "error": "not-in-world" };
|
||||
|
||||
// We must be in the world
|
||||
var cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position);
|
||||
if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld())
|
||||
return { "error": "not-in-world" };
|
||||
|
||||
// Target must be within range
|
||||
var posTarget = cmpPositionTarget.GetPosition();
|
||||
var posSelf = cmpPositionSelf.GetPosition();
|
||||
var dist2 = hypot2(posTarget.x - posSelf.x, posTarget.z - posSelf.z);
|
||||
// TODO: ought to be distance to closest point in footprint, not to center
|
||||
var maxrange = +this.template.Range;
|
||||
if (dist2 > maxrange*maxrange)
|
||||
return { "error": "out-of-range", "maxrange": maxrange };
|
||||
|
||||
return {};
|
||||
return { "max": +this.template.Range, "min": 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Attack the target entity. This should only be called after a successful CheckRange,
|
||||
* Attack the target entity. This should only be called after a successful range check,
|
||||
* and should only be called after GetTimers().repeat msec has passed since the last
|
||||
* call to PerformAttack.
|
||||
*/
|
||||
|
@ -6,16 +6,26 @@ Cost.prototype.Init = function()
|
||||
|
||||
Cost.prototype.GetPopCost = function()
|
||||
{
|
||||
if ('Population' in this.template)
|
||||
if ("Population" in this.template)
|
||||
return +this.template.Population;
|
||||
return 0;
|
||||
};
|
||||
|
||||
Cost.prototype.GetPopBonus = function()
|
||||
{
|
||||
if ('PopulationBonus' in this.template)
|
||||
if ("PopulationBonus" in this.template)
|
||||
return +this.template.PopulationBonus;
|
||||
return 0;
|
||||
};
|
||||
|
||||
Cost.prototype.GetResourceCosts = function()
|
||||
{
|
||||
return {
|
||||
"food": +(this.template.Resources.food || 0),
|
||||
"wood": +(this.template.Resources.wood || 0),
|
||||
"stone": +(this.template.Resources.stone || 0),
|
||||
"metal": +(this.template.Resources.metal || 0)
|
||||
};
|
||||
};
|
||||
|
||||
Engine.RegisterComponentType(IID_Cost, "Cost", Cost);
|
||||
|
@ -20,7 +20,8 @@ GuiInterface.prototype.GetSimulationState = function(player)
|
||||
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
|
||||
var player = {
|
||||
"popCount": cmpPlayer.GetPopulationCount(),
|
||||
"popLimit": cmpPlayer.GetPopulationLimit()
|
||||
"popLimit": cmpPlayer.GetPopulationLimit(),
|
||||
"resourceCounts": cmpPlayer.GetResourceCounts()
|
||||
};
|
||||
ret.players.push(player);
|
||||
}
|
||||
@ -62,6 +63,22 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
|
||||
ret.player = cmpOwnership.GetOwner();
|
||||
}
|
||||
|
||||
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
|
||||
if (cmpResourceSupply)
|
||||
{
|
||||
ret.resourceSupply = {
|
||||
"max": cmpResourceSupply.GetMaxAmount(),
|
||||
"amount": cmpResourceSupply.GetCurrentAmount(),
|
||||
"type": cmpResourceSupply.GetType()
|
||||
};
|
||||
}
|
||||
|
||||
var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
|
||||
if (cmpResourceGatherer)
|
||||
{
|
||||
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,12 @@ Player.prototype.Init = function()
|
||||
this.civ = "celt";
|
||||
this.popCount = 0;
|
||||
this.popLimit = 50;
|
||||
this.resourceCount = {
|
||||
"food": 100,
|
||||
"wood": 50,
|
||||
"metal": 0,
|
||||
"stone": 0
|
||||
};
|
||||
};
|
||||
|
||||
Player.prototype.SetPlayerID = function(id)
|
||||
@ -24,6 +30,16 @@ Player.prototype.GetPopulationLimit = function()
|
||||
return this.popLimit;
|
||||
};
|
||||
|
||||
Player.prototype.GetResourceCounts = function()
|
||||
{
|
||||
return this.resourceCount;
|
||||
};
|
||||
|
||||
Player.prototype.AddResources = function(type, amount)
|
||||
{
|
||||
this.resourceCount[type] += (+amount);
|
||||
};
|
||||
|
||||
Player.prototype.OnGlobalOwnershipChanged = function(msg)
|
||||
{
|
||||
if (msg.from == this.playerID)
|
||||
|
@ -0,0 +1,49 @@
|
||||
function ResourceGatherer() {}
|
||||
|
||||
ResourceGatherer.prototype.Init = function()
|
||||
{
|
||||
};
|
||||
|
||||
ResourceGatherer.prototype.GetGatherRates = function()
|
||||
{
|
||||
var ret = {};
|
||||
for (var r in this.template.Rates)
|
||||
ret[r] = this.template.Rates[r] * this.template.BaseSpeed;
|
||||
return ret;
|
||||
};
|
||||
|
||||
ResourceGatherer.prototype.GetRange = function()
|
||||
{
|
||||
return { "max": 4, "min": 0 };
|
||||
// maybe this should depend on the unit or target or something?
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather from the target entity. This should only be called after a successful range check,
|
||||
* and if the target has a compatible ResourceSupply.
|
||||
* It should be called at a rate of once per second.
|
||||
*/
|
||||
ResourceGatherer.prototype.PerformGather = function(target)
|
||||
{
|
||||
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
|
||||
var type = cmpResourceSupply.GetType();
|
||||
|
||||
var rate;
|
||||
if (type.specific && this.template.Rates[type.generic+"."+type.specific])
|
||||
rate = this.template.Rates[type.generic+"."+type.specific] * this.template.BaseSpeed;
|
||||
else
|
||||
rate = this.template.Rates[type.generic] * this.template.BaseSpeed;
|
||||
|
||||
var status = cmpResourceSupply.TakeResources(rate);
|
||||
|
||||
// Give the gathered resources to the player
|
||||
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);
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
|
||||
Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);
|
@ -0,0 +1,42 @@
|
||||
function ResourceSupply() {}
|
||||
|
||||
ResourceSupply.prototype.Init = function()
|
||||
{
|
||||
// Current resource amount (non-negative; can be a fractional amount)
|
||||
this.amount = this.GetMaxAmount();
|
||||
};
|
||||
|
||||
ResourceSupply.prototype.GetMaxAmount = function()
|
||||
{
|
||||
return +this.template.Amount;
|
||||
};
|
||||
|
||||
ResourceSupply.prototype.GetCurrentAmount = function()
|
||||
{
|
||||
return this.amount;
|
||||
};
|
||||
|
||||
ResourceSupply.prototype.TakeResources = function(rate)
|
||||
{
|
||||
// Internally we handle fractional resource amounts (to be accurate
|
||||
// over long periods of time), but want to return integers (so players
|
||||
// have a nice simple integer amount of resources). So return the
|
||||
// difference between rounded values:
|
||||
|
||||
var old = this.amount;
|
||||
this.amount = Math.max(0, old - rate/1000);
|
||||
var change = Math.ceil(old) - Math.ceil(this.amount);
|
||||
// (use ceil instead of floor so that we continue returning non-zero values even if
|
||||
// 0 < amount < 1)
|
||||
return { "amount": change, "exhausted": (old == 0) };
|
||||
};
|
||||
|
||||
ResourceSupply.prototype.GetType = function()
|
||||
{
|
||||
if (this.template.Subtype)
|
||||
return { "generic": this.template.Type, "specific": this.template.Subtype };
|
||||
else
|
||||
return { "generic": this.template.Type };
|
||||
};
|
||||
|
||||
Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply);
|
@ -1,13 +1,14 @@
|
||||
/*
|
||||
|
||||
This is currently just a very simplistic state machine that lets units be commanded around
|
||||
and then autonomously carry out the orders.
|
||||
and then autonomously carry out the orders. It might need to be entirely redesigned.
|
||||
|
||||
*/
|
||||
|
||||
const STATE_IDLE = 0;
|
||||
const STATE_WALKING = 1;
|
||||
const STATE_ATTACKING = 2;
|
||||
const STATE_GATHERING = 3;
|
||||
|
||||
/* Attack process:
|
||||
* When starting attack:
|
||||
@ -25,6 +26,11 @@ const STATE_ATTACKING = 2;
|
||||
* faster-than-normal attacks)
|
||||
*/
|
||||
|
||||
/* 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)
|
||||
*/
|
||||
|
||||
function UnitAI() {}
|
||||
|
||||
UnitAI.prototype.Init = function()
|
||||
@ -38,6 +44,11 @@ UnitAI.prototype.Init = function()
|
||||
this.attackTimer = undefined;
|
||||
// Current target entity ID
|
||||
this.attackTarget = undefined;
|
||||
|
||||
// Timer for GatherTimeout
|
||||
this.gatherTimer = undefined;
|
||||
// Current target entity ID
|
||||
this.gatherTarget = undefined;
|
||||
};
|
||||
|
||||
//// Interface functions ////
|
||||
@ -57,33 +68,46 @@ UnitAI.prototype.Walk = function(x, z)
|
||||
|
||||
UnitAI.prototype.Attack = function(target)
|
||||
{
|
||||
// Verify that we're able to respond to Attack commands
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
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.attackTarget = target;
|
||||
this.MoveToTarget(this.attackTarget);
|
||||
this.MoveToTarget(target, cmpAttack.GetRange());
|
||||
this.state = STATE_ATTACKING;
|
||||
};
|
||||
|
||||
// Cancel any previous attack timer
|
||||
if (this.attackTimer)
|
||||
{
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
cmpTimer.CancelTimer(this.attackTimer);
|
||||
this.attackTimer = undefined;
|
||||
}
|
||||
UnitAI.prototype.Gather = function(target)
|
||||
{
|
||||
// Verify that we're able to respond to Gather commands
|
||||
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
||||
if (!cmpResourceGatherer)
|
||||
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.gatherTarget = target;
|
||||
this.MoveToTarget(target, cmpResourceGatherer.GetRange());
|
||||
this.state = STATE_GATHERING;
|
||||
};
|
||||
|
||||
//// Message handlers ////
|
||||
|
||||
UnitAI.prototype.OnDestroy = function()
|
||||
{
|
||||
if (this.attackTimer)
|
||||
{
|
||||
cmpTimer.CancelTimer(this.attackTimer);
|
||||
this.attackTimer = undefined;
|
||||
}
|
||||
// Clean up any timers that are now obsolete
|
||||
this.CancelTimers();
|
||||
};
|
||||
|
||||
UnitAI.prototype.OnMotionChanged = function(msg)
|
||||
@ -123,11 +147,78 @@ UnitAI.prototype.OnMotionChanged = function(msg)
|
||||
// Start the idle animation before we switch to the attack
|
||||
this.SelectAnimation("idle");
|
||||
}
|
||||
else if (this.state == STATE_GATHERING)
|
||||
{
|
||||
// We were gathering, and have stopped moving
|
||||
// => check if we can still reach the target now
|
||||
|
||||
if (!this.MoveIntoGatherRange())
|
||||
return;
|
||||
|
||||
// In range, so perform the gathering
|
||||
|
||||
var cmpResourceSupply = Engine.QueryInterface(this.gatherTarget, IID_ResourceSupply);
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
|
||||
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {});
|
||||
|
||||
// Start the gather animation
|
||||
var type = cmpResourceSupply.GetType();
|
||||
var anim = "gather_" + (type.specific || type.generic);
|
||||
this.SelectAnimation(anim);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//// Private functions ////
|
||||
|
||||
function hypot2(x, y)
|
||||
{
|
||||
return x*x + y*y;
|
||||
}
|
||||
|
||||
UnitAI.prototype.CheckRange = function(target, range)
|
||||
{
|
||||
// Target must be in the world
|
||||
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
|
||||
if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld())
|
||||
return { "error": "not-in-world" };
|
||||
|
||||
// We must be in the world
|
||||
var cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position);
|
||||
if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld())
|
||||
return { "error": "not-in-world" };
|
||||
|
||||
// Target must be within range
|
||||
var posTarget = cmpPositionTarget.GetPosition();
|
||||
var posSelf = cmpPositionSelf.GetPosition();
|
||||
var dist2 = hypot2(posTarget.x - posSelf.x, posTarget.z - posSelf.z);
|
||||
// TODO: ought to be distance to closest point in footprint, not to center
|
||||
// The +4 is a hack to give a ~1 tile tolerance, because the pathfinder doesn't
|
||||
// always get quite close enough to the target
|
||||
if (dist2 > (range.max+4)*(range.max+4))
|
||||
return { "error": "out-of-range" };
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UnitAI.prototype.CancelTimers = function()
|
||||
{
|
||||
if (this.attackTimer)
|
||||
{
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
cmpTimer.CancelTimer(this.attackTimer);
|
||||
this.attackTimer = undefined;
|
||||
}
|
||||
|
||||
if (this.gatherTimer)
|
||||
{
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
cmpTimer.CancelTimer(this.gatherTimer);
|
||||
this.gatherTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to move into range of the attack target.
|
||||
* Returns true if it's already in range.
|
||||
@ -135,15 +226,42 @@ UnitAI.prototype.OnMotionChanged = function(msg)
|
||||
UnitAI.prototype.MoveIntoAttackRange = function()
|
||||
{
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
var range = cmpAttack.GetRange();
|
||||
|
||||
var rangeStatus = cmpAttack.CheckRange(this.attackTarget);
|
||||
var rangeStatus = this.CheckRange(this.attackTarget, 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.attackTarget);
|
||||
this.MoveToTarget(this.attackTarget, 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);
|
||||
var range = cmpResourceGatherer.GetRange();
|
||||
|
||||
var rangeStatus = this.CheckRange(this.gatherTarget, 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.gatherTarget, range);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -166,7 +284,7 @@ UnitAI.prototype.SelectAnimation = function(name, once, speed)
|
||||
cmpVisual.SelectAnimation(name, once, speed);
|
||||
};
|
||||
|
||||
UnitAI.prototype.MoveToTarget = function(target)
|
||||
UnitAI.prototype.MoveToTarget = function(target, range)
|
||||
{
|
||||
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
|
||||
if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld())
|
||||
@ -175,7 +293,7 @@ UnitAI.prototype.MoveToTarget = function(target)
|
||||
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
||||
|
||||
var pos = cmpPositionTarget.GetPosition();
|
||||
cmpMotion.MoveToPoint(pos.x, pos.z, 0, 1);
|
||||
cmpMotion.MoveToPoint(pos.x, pos.z, range.min, range.max);
|
||||
};
|
||||
|
||||
UnitAI.prototype.AttackTimeout = function(data)
|
||||
@ -205,4 +323,33 @@ UnitAI.prototype.AttackTimeout = function(data)
|
||||
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", timers.repeat, 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;
|
||||
|
||||
// Gather from the target
|
||||
var status = cmpResourceGatherer.PerformGather(this.gatherTarget);
|
||||
|
||||
// If the resource is exhausted, then stop and go back to idle
|
||||
if (status.exhausted)
|
||||
{
|
||||
this.state = STATE_IDLE;
|
||||
this.SelectAnimation("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set a timer to gather again
|
||||
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, data);
|
||||
};
|
||||
|
||||
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|
||||
|
@ -0,0 +1 @@
|
||||
Engine.RegisterInterface("ResourceGatherer");
|
@ -0,0 +1 @@
|
||||
Engine.RegisterInterface("ResourceSupply");
|
@ -1,6 +1,8 @@
|
||||
Engine.LoadComponentScript("interfaces/Attack.js");
|
||||
Engine.LoadComponentScript("interfaces/Builder.js");
|
||||
Engine.LoadComponentScript("interfaces/Health.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
|
||||
Engine.LoadComponentScript("GuiInterface.js");
|
||||
|
||||
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
|
||||
@ -18,16 +20,18 @@ AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
||||
|
||||
AddMock(100, IID_Player, {
|
||||
GetPopulationCount: function() { return 10; },
|
||||
GetPopulationLimit: function() { return 20; }
|
||||
GetPopulationLimit: function() { return 20; },
|
||||
GetResourceCounts: function() { return { "food": 100 }; }
|
||||
});
|
||||
|
||||
AddMock(101, IID_Player, {
|
||||
GetPopulationCount: function() { return 40; },
|
||||
GetPopulationLimit: function() { return 30; }
|
||||
GetPopulationLimit: function() { return 30; },
|
||||
GetResourceCounts: function() { return { "food": 200 }; }
|
||||
});
|
||||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
|
||||
players: [{popCount:10, popLimit:20}, {popCount:40, popLimit:30}]
|
||||
players: [{popCount:10, popLimit:20, resourceCounts:{food:100}}, {popCount:40, popLimit:30, resourceCounts:{food:200}}]
|
||||
});
|
||||
|
||||
|
||||
|
@ -24,6 +24,16 @@ function ProcessCommand(player, cmd)
|
||||
}
|
||||
break;
|
||||
|
||||
case "gather":
|
||||
for each (var ent in cmd.entities)
|
||||
{
|
||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
if (!ai)
|
||||
continue;
|
||||
ai.Gather(cmd.target);
|
||||
}
|
||||
break;
|
||||
|
||||
case "construct":
|
||||
// TODO: this should do all sorts of stuff with foundations and resource costs etc
|
||||
var ent = Engine.AddEntity(cmd.template);
|
||||
|
@ -94,6 +94,9 @@ public:
|
||||
/// Equality.
|
||||
bool operator==(CFixed n) const { return (value == n.value); }
|
||||
|
||||
/// Inequality.
|
||||
bool operator!=(CFixed n) const { return (value != n.value); }
|
||||
|
||||
/// Numeric comparison.
|
||||
bool operator<=(CFixed n) const { return (value <= n.value); }
|
||||
|
||||
|
@ -44,7 +44,9 @@ public:
|
||||
|
||||
bool m_HasTarget;
|
||||
ICmpPathfinder::Path m_Path;
|
||||
entity_pos_t m_TargetX, m_TargetZ; // these values contain undefined junk if !HasTarget
|
||||
// These values contain undefined junk if !HasTarget:
|
||||
entity_pos_t m_TargetX, m_TargetZ; // currently-selected waypoint
|
||||
entity_pos_t m_FinalTargetX, m_FinalTargetZ; // final target center (used to face towards it)
|
||||
|
||||
enum
|
||||
{
|
||||
@ -76,6 +78,7 @@ public:
|
||||
// TODO: m_Path
|
||||
serialize.NumberFixed_Unbounded("target x", m_TargetX);
|
||||
serialize.NumberFixed_Unbounded("target z", m_TargetZ);
|
||||
// TODO: m_FinalTargetAngle
|
||||
}
|
||||
|
||||
// TODO: m_State
|
||||
@ -103,9 +106,9 @@ public:
|
||||
|
||||
if (m_State == STOPPING)
|
||||
{
|
||||
m_State = IDLE;
|
||||
CMessageMotionChanged msg(CFixed_23_8::FromInt(0));
|
||||
context.GetComponentManager().PostMessage(GetEntityId(), msg);
|
||||
m_State = IDLE;
|
||||
}
|
||||
|
||||
Move(context, dt);
|
||||
@ -194,6 +197,8 @@ public:
|
||||
}
|
||||
else
|
||||
{
|
||||
m_FinalTargetX = x;
|
||||
m_FinalTargetZ = z;
|
||||
PickNextWaypoint(pos);
|
||||
}
|
||||
}
|
||||
@ -245,6 +250,16 @@ void CCmpUnitMotion::Move(const CSimContext& context, CFixed_23_8 dt)
|
||||
if (m_Path.m_Waypoints.empty())
|
||||
{
|
||||
cmpPosition->MoveTo(target.X, target.Z);
|
||||
|
||||
// If we didn't reach the final goal, point towards it now
|
||||
if (target.X != m_FinalTargetX || target.Z != m_FinalTargetZ)
|
||||
{
|
||||
CFixedVector3D final(m_FinalTargetX, CFixed_23_8::FromInt(0), m_FinalTargetZ);
|
||||
CFixedVector3D finalOffset = final - target;
|
||||
entity_angle_t angle = atan2_approx(finalOffset.X, finalOffset.Z);
|
||||
cmpPosition->TurnTo(angle);
|
||||
}
|
||||
|
||||
m_HasTarget = false;
|
||||
SwitchState(context, IDLE);
|
||||
return;
|
||||
|
Loading…
Reference in New Issue
Block a user