1
0
forked from 0ad/0ad

# Support resource gathering in new simulation system

This was SVN commit r7322.
This commit is contained in:
Ykkrosh 2010-02-12 22:46:53 +00:00
parent 321cc8ae8f
commit f8aca33a14
14 changed files with 365 additions and 56 deletions

View File

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

View File

@ -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.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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