# Primitive melee combat support in new simulation system

This was SVN commit r7309.
This commit is contained in:
Ykkrosh 2010-02-05 22:00:39 +00:00
parent c50fe9b8c0
commit b21e798243
19 changed files with 474 additions and 19 deletions

View file

@ -13,8 +13,74 @@ var inputState = INPUT_NORMAL;
var placementEntity = "";
var mouseX = 0;
var mouseY = 0;
function updateCursor()
{
var action = determineAction(mouseX, mouseY);
if (action)
{
if (action.type != "move")
{
Engine.SetCursor("action-" + action.type);
return;
}
}
Engine.SetCursor("arrow-default");
}
/**
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
*/
function determineAction(x, y)
{
var selection = getEntitySelection();
// No action if there's no selection
if (!selection.length)
return;
// If the selection isn't friendly units, no action
var entState = Engine.GuiInterfaceCall("GetEntityState", selection[0]);
var player = Engine.GetPlayerID();
if (entState.player != player)
return;
var targets = Engine.PickEntitiesAtPoint(x, y);
// If there's no unit, just walk
if (!targets.length)
return {"type": "move"};
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
var targetState = Engine.GuiInterfaceCall("GetEntityState", targets[0]);
// Different owner -> attack
if (entState.attack && targetState.player != player)
return {"type": "attack", "target": targets[0]};
// TODO: need more actions
// If we don't do anything more specific, just walk
return {"type": "move"};
}
function handleInputBeforeGui(ev)
{
switch (ev.type)
{
case "mousebuttonup":
case "mousebuttondown":
case "mousemotion":
mouseX = ev.x;
mouseY = ev.y;
break;
}
return false;
}
@ -54,17 +120,25 @@ function handleInputAfterGui(ev)
resetEntitySelection();
addEntitySelection([ents[0]]);
Engine.PostNetworkCommand({"type": "spin", "entities": [ents[0]]});
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
var ents = getEntitySelection();
if (ents.length)
var action = determineAction(ev.x, ev.y);
if (!action)
break;
var selection = getEntitySelection();
switch (action.type)
{
case "move":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
Engine.PostNetworkCommand({"type": "walk", "entities": ents, "x": target.x, "z": target.z});
Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z});
return true;
case "attack":
Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target});
return true;
}
}

View file

@ -46,6 +46,6 @@ function getEntitySelection()
{
var ents = [];
for (var ent in g_Selection)
ents.push(ent);
ents.push(+ent); // convert from string to number and push
return ents;
}

View file

@ -3,6 +3,11 @@ function init()
updateDebug();
}
function onTick()
{
updateCursor();
}
function onSimulationUpdate()
{
updateDebug();

View file

@ -32,6 +32,10 @@
initSession();
</action>
<action on="Tick">
onTick();
</action>
<action on="SimulationUpdate">
onSimulationUpdate();
</action>

View file

@ -0,0 +1,23 @@
function Armour() {}
Armour.prototype.Init = function()
{
};
Armour.prototype.TakeDamage = function(hack, pierce, crush)
{
// Adjust damage values based on armour
// (Default armour values to 0 if undefined)
var adjHack = Math.max(0, hack - (this.template.Hack || 0));
var adjPierce = Math.max(0, pierce - (this.template.Pierce || 0));
var adjCrush = Math.max(0, crush - (this.template.Crush || 0));
// Total is sum of individual damages, with minimum damage 1
var total = Math.max(1, adjHack + adjPierce + adjCrush);
// Reduce health
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
cmpHealth.Reduce(total);
};
Engine.RegisterComponentType(IID_DamageReceiver, "Armour", Armour);

View file

@ -0,0 +1,76 @@
function Attack() {}
Attack.prototype.Init = function()
{
};
/*
* TODO: to handle secondary attacks in the future, what we might do is
* add a 'mode' parameter to most of these functions, to indicate which
* attack mode we're trying to use, and some other function that allows
* UnitAI to pick the best attack mode (based on range, damage, etc)
*/
Attack.prototype.GetTimers = function()
{
var prepare = +(this.template.PrepareTime || 0);
var repeat = +(this.template.RepeatTime || 1000);
return { "prepare": prepare, "repeat": repeat, "recharge": repeat - prepare };
};
Attack.prototype.GetAttackStrengths = function()
{
// Convert attack values to numbers, default 0 if unspecified
return {
hack: +(this.template.Hack || 0),
pierce: +(this.template.Pierce || 0),
crush: +(this.template.Crush || 0)
};
};
function hypot2(x, y)
{
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 {};
}
/**
* Attack the target entity. This should only be called after a successful CheckRange,
* and should only be called after GetTimers().repeat msec has passed since the last
* call to PerformAttack.
*/
Attack.prototype.PerformAttack = function(target)
{
var strengths = this.GetAttackStrengths();
// Inflict damage on the target
var cmpDamageReceiver = Engine.QueryInterface(target, IID_DamageReceiver);
if (!cmpDamageReceiver)
return;
cmpDamageReceiver.TakeDamage(strengths.hack, strengths.pierce, strengths.crush);
};
Engine.RegisterComponentType(IID_Attack, "Attack", Attack);

View file

@ -38,12 +38,30 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"position": cmpPosition.GetPosition()
};
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
}
var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
ret.attack = cmpAttack.GetAttackStrengths();
}
var cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
{
ret.buildEntities = cmpBuilder.GetEntitiesList();
}
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
{
ret.player = cmpOwnership.GetOwner();
}
return ret;
};

View file

@ -0,0 +1,26 @@
function Health() {}
Health.prototype.Init = function()
{
this.hitpoints = +this.template.Max;
};
Health.prototype.GetHitpoints = function()
{
return this.hitpoints;
};
Health.prototype.Reduce = function(amount)
{
if (amount >= this.hitpoints)
{
this.hitpoints = 0;
// TODO: need to destroy this entity, set up a corpse, etc
}
else
{
this.hitpoints -= amount;
}
}
Engine.RegisterComponentType(IID_Health, "Health", Health);

View file

@ -0,0 +1,56 @@
function Timer() {}
Timer.prototype.Init = function()
{
this.id = 0;
this.time = 0;
this.timers = {};
};
Timer.prototype.GetTime = function()
{
return this.time;
}
Timer.prototype.OnUpdate = function(msg)
{
var dt = Math.round(msg.turnLength * 1000);
this.time += dt;
// Collect the timers that need to run
// (We do this in two stages to avoid deleting from the timer list while
// we're in the middle of iterating through it)
var run = [];
for (var id in this.timers)
{
if (this.timers[id][3] <= this.time)
run.push(id);
}
for each (var id in run)
{
var t = this.timers[id];
var cmp = Engine.QueryInterface(t[0], t[1]);
try {
cmp[t[2]](t[4]);
} catch (e) {
print("Error in timer on entity "+t[0]+", IID "+t[1]+", function "+t[2]+": "+e);
// TODO: should report in an error log
}
delete this.timers[id];
}
}
Timer.prototype.SetTimeout = function(ent, iid, funcname, time, data)
{
var id = ++this.id;
this.timers[id] = [ent, iid, funcname, this.time + time, data];
return id;
};
Timer.prototype.CancelTimer = function(id)
{
delete this.timers[id];
};
Engine.RegisterComponentType(IID_Timer, "Timer", Timer);

View file

@ -0,0 +1,140 @@
/*
This is currently just a very simplistic state machine that lets units be commanded around
and then autonomously carry out the orders.
*/
const STATE_IDLE = 0;
const STATE_WALKING = 1;
const STATE_ATTACKING = 2;
/* Attack process:
* When starting attack:
* Activate attack animation (with appropriate repeat speed and offset)
* Set this.attackTimer to run at maximum of:
* GetTimers().prepare msec from now
* this.attackRechargeTime
* Loop:
* Wait for the timer
* Perform the attack
* Set this.attackRechargeTime to now plus GetTimers().recharge
* Set this.attackTimer to run after GetTimers().repeat
* At any point it's safe to cancel the attack and switch to a different action
* (The rechargeTime is to prevent people spamming the attack command and getting
* faster-than-normal attacks)
*/
function UnitAI() {}
UnitAI.prototype.Init = function()
{
this.state = STATE_IDLE;
// The earliest time at which we'll have 'recovered' from the previous attack, and
// can start preparing a new attack
this.attackRechargeTime = 0;
// Timer for AttackTimeout
this.attackTimer = undefined;
};
UnitAI.prototype.OnDestroy = function()
{
if (this.attackTimer)
{
cmpTimer.CancelTimer(this.attackTimer);
this.attackTimer = undefined;
}
};
//// Interface functions ////
UnitAI.prototype.Walk = function(x, z)
{
var motion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!motion)
return;
motion.MoveToPoint(x, z);
this.state = STATE_WALKING;
};
UnitAI.prototype.Attack = function(target)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
// Cancel any previous attack timer
if (this.attackTimer)
cmpTimer.CancelTimer(this.attackTimer);
// TODO: start the attack animation here
// TODO: should check the range and move closer before attempting to attack
// Perform the attack after the prepare time, but not before the previous attack's recharge
var timers = cmpAttack.GetTimers();
var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime());
var data = { "target": target, "timers": timers };
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, data);
this.state = STATE_ATTACKING;
};
//// Private functions ////
UnitAI.prototype.MoveToTarget = function(target)
{
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld())
return;
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
var pos = cmpPositionTarget.GetPosition();
cmpMotion.MoveToPoint(pos.x, pos.z);
};
UnitAI.prototype.AttackTimeout = function(data)
{
// If we stopped attacking before this timeout, then don't do any processing here
if (this.state != STATE_ATTACKING)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Check if we can still reach the target
var rangeStatus = cmpAttack.CheckRange(data.target);
if (rangeStatus.error)
{
if (rangeStatus.error == "out-of-range")
{
// Out of range => need to move closer
this.MoveToTarget(data.target);
// Try again in a couple of seconds
// (TODO: ought to have a cleverer way of detecting once we're back in range)
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", 2000, data);
return;
}
// Otherwise it's impossible to reach the target, so give up
// and switch back to idle
this.state = STATE_IDLE;
return;
}
// Hit the target
cmpAttack.PerformAttack(data.target);
// Set a timer to hit the target again
this.attackRechargeTime = cmpTimer.GetTime() + data.timers.recharge;
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", data.timers.repeat, data);
};
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,6 @@
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("GuiInterface.js");
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
@ -35,6 +37,12 @@ AddMock(10, IID_Position, {
}
});
AddMock(10, IID_Health, {
GetHitpoints: function() {
return 50;
}
});
AddMock(10, IID_Builder, {
GetEntitiesList: function() {
return ["test1", "test2"];
@ -45,5 +53,6 @@ var state = cmp.GetEntityState(-1, 10);
TS_ASSERT_UNEVAL_EQUALS(state, {
template: "example",
position: {x:1, y:2, z:3},
hitpoints: 50,
buildEntities: ["test1", "test2"]
});

View file

@ -4,23 +4,23 @@ function ProcessCommand(player, cmd)
switch (cmd.type)
{
case "spin":
for each (var ent in cmd.entities)
{
var pos = Engine.QueryInterface(ent, IID_Position);
if (! pos)
continue;
pos.SetYRotation(pos.GetRotation().y + 1);
}
break;
case "walk":
for each (var ent in cmd.entities)
{
var motion = Engine.QueryInterface(ent, IID_UnitMotion);
if (! motion)
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (!ai)
continue;
motion.MoveToPoint(cmd.x, cmd.z);
ai.Walk(cmd.x, cmd.z);
}
break;
case "attack":
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (!ai)
continue;
ai.Attack(cmd.target);
}
break;

View file

@ -26,6 +26,7 @@
#include "ps/Game.h"
#include "ps/Overlay.h"
#include "ps/Player.h"
#include "ps/GameSetup/Config.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpGuiInterface.h"
@ -168,6 +169,20 @@ CFixedVector3D GetTerrainAtPoint(void* UNUSED(cbdata), int x, int y)
return CFixedVector3D(CFixed_23_8::FromFloat(pos.X), CFixed_23_8::FromFloat(pos.Y), CFixed_23_8::FromFloat(pos.Z));
}
std::wstring SetCursor(void* UNUSED(cbdata), std::wstring name)
{
std::wstring old = g_CursorName;
g_CursorName = name;
return old;
}
int GetPlayerID(void* UNUSED(cbdata))
{
if (g_Game && g_Game->GetLocalPlayer())
return g_Game->GetLocalPlayer()->GetPlayerID();
return -1;
}
} // namespace
void GuiScriptingInit(ScriptInterface& scriptInterface)
@ -187,4 +202,7 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint");
scriptInterface.RegisterFunction<CFixedVector3D, int, int, &GetTerrainAtPoint>("GetTerrainAtPoint");
// Misc functions
scriptInterface.RegisterFunction<std::wstring, std::wstring, &SetCursor>("SetCursor");
scriptInterface.RegisterFunction<int, &GetPlayerID>("GetPlayerID");
}

View file

@ -73,6 +73,7 @@ public:
LOAD_SCRIPTED_COMPONENT("GuiInterface");
LOAD_SCRIPTED_COMPONENT("PlayerManager");
LOAD_SCRIPTED_COMPONENT("Timer");
#undef LOAD_SCRIPTED_COMPONENT
}