function AnimalAI() {} AnimalAI.prototype.Schema = "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "skittish" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; var AnimalFsmSpec = { "MoveCompleted": function() { // ignore spurious movement messages // (these can happen when stopping moving at the same time // as switching states) }, "MoveStarted": function() { // ignore spurious movement messages }, "HealthChanged": function(msg) { // If we died (got reduced to 0 hitpoints), stop the AI and act like a corpse if (msg.to == 0) this.SetNextState("CORPSE"); }, "CORPSE": { "enter": function() { this.StopMoving(); }, "Attacked": function(msg) { // Do nothing, because we're dead already }, }, "SKITTISH": { "Attacked": function(msg) { // If someone's attacking us, then run away this.MoveAwayFrom(msg.data.attacker, +this.template.FleeDistance); this.SetNextState("FLEEING"); this.PlaySound("panic"); }, "ROAMING": { "enter": function() { // Walk in a random direction this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { this.SetNextState("FEEDING"); }, "MoveCompleted": function() { this.MoveRandomly(+this.template.RoamDistance); }, }, "FEEDING": { "enter": function() { // Stop and eat for a while this.SelectAnimation("feeding"); this.StopMoving(); this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax)); }, "leave": function() { this.StopTimer(); }, "MoveCompleted": function() { }, "Timer": function(msg) { this.SetNextState("ROAMING"); }, }, "FLEEING": { "enter": function() { // Run quickly var speed = this.GetRunSpeed(); this.SelectAnimation("run", false, speed); this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed this.SetMoveSpeed(this.GetWalkSpeed()); }, "MoveCompleted": function() { // When we've run far enough, go back to the roaming state this.SetNextState("ROAMING"); }, }, }, "PASSIVE": { "Attacked": function(msg) { // Do nothing, just let them kill us }, "ROAMING": { "enter": function() { // Walk in a random direction this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { this.SetNextState("FEEDING"); }, "MoveCompleted": function() { this.MoveRandomly(+this.template.RoamDistance); }, }, "FEEDING": { "enter": function() { // Stop and eat for a while this.SelectAnimation("feeding"); this.StopMoving(); this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax)); }, "leave": function() { this.StopTimer(); }, "MoveCompleted": function() { }, "Timer": function(msg) { this.SetNextState("ROAMING"); }, }, }, }; var AnimalFsm = new FSM(AnimalFsmSpec); AnimalAI.prototype.Init = function() { }; // FSM linkage functions: AnimalAI.prototype.OnCreate = function() { var startingState = this.template.NaturalBehaviour; startingState = startingState.toUpperCase(startingState); if (startingState == "SKITTISH") startingState = startingState + ".FEEDING"; else startingState = "PASSIVE.FEEDING"; AnimalFsm.Init(this, startingState); }; AnimalAI.prototype.SetNextState = function(state) { AnimalFsm.SetNextState(this, state); }; AnimalAI.prototype.DeferMessage = function(msg) { AnimalFsm.DeferMessage(this, msg); }; AnimalAI.prototype.OnMotionChanged = function(msg) { if (msg.starting && !msg.error) { AnimalFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); } else if (!msg.starting || msg.error) { AnimalFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); } }; AnimalAI.prototype.OnAttacked = function(msg) { AnimalFsm.ProcessMessage(this, {"type": "Attacked", "data": msg}); }; AnimalAI.prototype.OnHealthChanged = function(msg) { AnimalFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to}); }; AnimalAI.prototype.TimerHandler = function(data, lateness) { AnimalFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness}); }; // Functions to be called by the FSM: AnimalAI.prototype.GetWalkSpeed = function() { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpMotion.GetWalkSpeed(); }; AnimalAI.prototype.GetRunSpeed = function() { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpMotion.GetRunSpeed(); }; AnimalAI.prototype.PlaySound = function(name) { PlaySound(name, this.entity); }; AnimalAI.prototype.SelectAnimation = function(name, once, speed, sound) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; var soundgroup; if (sound) { var cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (cmpSound) soundgroup = cmpSound.GetSoundGroup(sound); } // Set default values if unspecified if (typeof once == "undefined") once = false; if (typeof speed == "undefined") speed = 1.0; if (typeof soundgroup == "undefined") soundgroup = ""; cmpVisual.SelectAnimation(name, once, speed, soundgroup); }; AnimalAI.prototype.MoveRandomly = function(distance) { // We want to walk in a random direction, but avoid getting stuck // in obstacles or narrow spaces. // So pick a circular range from approximately our current position, // and move outwards to the nearest point on that circle, which will // lead to us avoiding obstacles and moving towards free space. // TODO: we probably ought to have a 'home' point, and drift towards // that, so we don't spread out all across the whole map var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition) return; if (!cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); var jitter = 0.5; // Randomly adjust the range's center a bit, so we tend to prefer // moving in random directions (if there's nothing in the way) var tx = pos.x + (2*Math.random()-1)*jitter; var tz = pos.z + (2*Math.random()-1)*jitter; var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.MoveToPointRange(tx, tz, distance, distance); }; AnimalAI.prototype.MoveAwayFrom = function(ent, distance) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.MoveToAttackRange(ent, distance, distance); }; AnimalAI.prototype.StopMoving = function() { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.StopMoving(); }; AnimalAI.prototype.SetMoveSpeed = function(speed) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.SetSpeed(speed); }; AnimalAI.prototype.StartTimer = function(interval, data) { if (this.timer) error("Called StartTimer when there's already an active timer"); var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_AnimalAI, "TimerHandler", interval, data); }; AnimalAI.prototype.StopTimer = function() { if (!this.timer) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; Engine.RegisterComponentType(IID_AnimalAI, "AnimalAI", AnimalAI);