Index: ps/trunk/binaries/data/mods/public/simulation/components/AnimalAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AnimalAI.js (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/components/AnimalAI.js (nonexistent) @@ -1,387 +0,0 @@ -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 - }, - - "LeaveFoundation": function(msg) { - // We can't walk away from the foundation (since we're dead), - // but we mustn't block its construction (since the builders would get stuck), - // and we don't want to trick gatherers into trying to reach us when - // we're stuck in the middle of a building, so just delete our corpse. - Engine.DestroyEntity(this.entity); - }, - }, - - "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"); - }, - - "LeaveFoundation": function(msg) { - // Run away from the foundation - this.MoveAwayFrom(msg.target, +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 - }, - - "LeaveFoundation": function(msg) { - // Walk away from the foundation - this.MoveAwayFrom(msg.target, 4); - this.SetNextState("FLEEING"); - }, - - "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() { - this.SelectAnimation("walk", false, this.GetWalkSpeed()); - }, - - "MoveCompleted": function() { - 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}); -}; - -AnimalAI.prototype.LeaveFoundation = function(target) -{ - AnimalFsm.ProcessMessage(this, {"type": "LeaveFoundation", "target": target}); -}; - -// 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.MoveToTargetRange(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); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AnimalAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AnimalAI.js (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AnimalAI.js (nonexistent) @@ -1 +0,0 @@ -Engine.RegisterInterface("AnimalAI"); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js (revision 8995) @@ -1,324 +1,344 @@ // Hierarchical finite state machine implementation. // // FSMs are specified as a JS data structure; // see e.g. UnitAI.js for an example of the syntax. // // FSMs are implicitly linked with an external object. // That object stores all FSM-related state. // (This means we can serialise FSM-based components as // plain old JS objects, with no need to serialise the complex // FSM structure itself or to add custom serialisation code.) /** FSM API: Users define the FSM behaviour like: var FsmSpec = { // Define some default message handlers: "MessageName1": function(msg) { // This function will be called in response to calls to // Fsm.ProcessMessage(this, { "type": "MessageName1", "data": msg }); // // In this function, 'this' is the component object passed into // ProcessMessage, so you can access 'this.propertyName' // and 'this.methodName()' etc. }, "MessageName2": function(msg) { // Another message handler. }, // Define the behaviour for the 'STATENAME' state: "STATENAME": { "MessageName1": function(msg) { // This overrides the previous MessageName1 that was // defined earlier, and will be called instead of it // in response to ProcessMessage. }, // We don't override MessageName2, so the default one // will be called instead. // Define the 'STATENAME.SUBSTATENAME' state: // (we support arbitrarily-nested hierarchies of states) "SUBSTATENAME": { "MessageName2": function(msg) { // Override the default MessageName2. // But we don't override MessageName1, so the one from // STATENAME will be used instead. }, "enter": function() { // This is a special function called when transitioning // into this state, or into a substate of this state. // // If it returns true, the transition will be aborted: // do this if you've called SetNextState inside this enter // handler, because otherwise the new state transition // will get mixed up with the previous ongoing one. // In normal cases, you can return false or nothing. }, "leave": function() { // Called when transitioning out of this state. }, }, + // Define a new state which is an exact copy of another + // state that is defined elsewhere in this FSM: + "OTHERSUBSTATENAME": "STATENAME.SUBSTATENAME", } } */ function FSM(spec) { // The (relatively) human-readable FSM specification needs to get // compiled into a more-efficient-to-execute version. // // In particular, message handling should require minimal // property lookups in the common case (even when the FSM has // a deeply nested hierarchy), and there should never be any // string manipulation at run-time. this.decompose = { "": [] }; /* 'decompose' will store: { "": [], "A": ["A"], "A.B": ["A", "A.B"], "A.B.C": ["A", "A.B", "A.B.C"], "A.B.D": ["A", "A.B", "A.B.D"], ... }; This is used when switching between states in different branches of the hierarchy, to determine the list of sub-states to leave/enter */ this.states = { }; /* 'states' will store: { ... "A": { "_name": "A", "_parent": "", "_refs": { // local -> global name lookups (for SetNextState) "B": "A.B", "B.C": "A.B.C", "B.D": "A.B.D", }, }, "A.B": { "_name": "A.B", "_parent": "A", "_refs": { "C": "A.B.C", "D": "A.B.D", }, "MessageType": function(msg) { ... }, }, "A.B.C": { "_name": "A.B.C", "_parent": "A.B", "_refs": {}, "enter": function() { ... }, "MessageType": function(msg) { ... }, }, "A.B.D": { "_name": "A.B.D", "_parent": "A.B", "_refs": {}, "enter": function() { ... }, "leave": function() { ... }, "MessageType": function(msg) { ... }, }, ... } */ function process(fsm, node, path, handlers) { + // Handle string references to nodes defined elsewhere in the FSM spec + if (typeof node === "string") + { + var refpath = node.split("."); + var refd = spec; + for each (var p in refpath) + { + refd = refd[p]; + if (!refd) + { + error("FSM node "+path.join(".")+" referred to non-defined node "+node); + return {}; + } + } + node = refd; + } + var state = {}; fsm.states[path.join(".")] = state; var newhandlers = {}; for (var e in handlers) newhandlers[e] = handlers[e]; state._name = path.join("."); state._parent = path.slice(0, -1).join("."); state._refs = {}; for (var key in node) { if (key === "enter" || key === "leave") { state[key] = node[key]; } else if (key.match(/^[A-Z]+$/)) { state._refs[key] = (state._name ? state._name + "." : "") + key; // (the rest of this will be handled later once we've grabbed // all the event handlers) } else { newhandlers[key] = node[key]; } } for (var e in newhandlers) state[e] = newhandlers[e]; for (var key in node) { if (key.match(/^[A-Z]+$/)) { var newpath = path.concat([key]); var decomposed = [newpath[0]]; for (var i = 1; i < newpath.length; ++i) decomposed.push(decomposed[i-1] + "." + newpath[i]); fsm.decompose[newpath.join(".")] = decomposed; var childstate = process(fsm, node[key], newpath, newhandlers); for (var r in childstate._refs) { var cname = key + "." + r; state._refs[cname] = childstate._refs[r]; } } } return state; } process(this, spec, [], {}); } FSM.prototype.Init = function(obj, initialState) { this.deferFromState = undefined; obj.fsmStateName = ""; obj.fsmNextState = undefined; this.SwitchToNextState(obj, initialState); }; FSM.prototype.SetNextState = function(obj, state) { obj.fsmNextState = state; }; FSM.prototype.ProcessMessage = function(obj, msg) { // warn("ProcessMessage(obj, "+uneval(msg)+")"); var func = this.states[obj.fsmStateName][msg.type]; if (!func) { error("Tried to process unhandled event '" + msg.type + "' in state '" + obj.fsmStateName + "'"); return; } func.apply(obj, [msg]); while (obj.fsmNextState) { var nextStateName = this.LookupState(obj.fsmStateName, obj.fsmNextState); obj.fsmNextState = undefined; if (nextStateName != obj.fsmStateName) this.SwitchToNextState(obj, nextStateName); } }; FSM.prototype.DeferMessage = function(obj, msg) { // We need to work out which sub-state we were running the message handler from, // and then try again in its parent state. var old = this.deferFromState; var from; if (old) // if we're recursively deferring and saved the last used state, use that from = old; else // if this is the first defer then we must have last processed the message in the current FSM state from = obj.fsmStateName; // Find and save the parent, for use in recursive defers this.deferFromState = this.states[from]._parent; // Run the function from the parent state var state = this.states[this.deferFromState]; var func = state[msg.type]; if (!func) error("Failed to defer event '" + msg.type + "' from state '" + obj.fsmStateName + "'"); func.apply(obj, [msg]); // Restore the changes we made this.deferFromState = old; // TODO: if an inherited handler defers, it calls exactly the same handler // on the parent state, which is probably useless and inefficient // NOTE: this will break if two units try to execute AI at the same time; // as long as AI messages are queue and processed asynchronously it should be fine }; FSM.prototype.LookupState = function(currentStateName, stateName) { // print("LookupState("+currentStateName+", "+stateName+")\n"); for (var s = currentStateName; s; s = this.states[s]._parent) if (stateName in this.states[s]._refs) return this.states[s]._refs[stateName]; return stateName; }; FSM.prototype.SwitchToNextState = function(obj, nextStateName) { var fromState = this.decompose[obj.fsmStateName]; var toState = this.decompose[nextStateName]; if (!toState) error("Tried to change to non-existent state '" + nextStateName + "'"); // Find the set of states in the hierarchy tree to leave then enter, // to traverse from the old state to the new one. // If any enter/leave function returns true then abort the process // (this lets them intercept the transition and start a new transition) for (var equalPrefix = 0; fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix) { } for (var i = fromState.length-1; i >= equalPrefix; --i) { var leave = this.states[fromState[i]].leave; if (leave) { obj.fsmStateName = fromState[i]; if (leave.apply(obj)) return; } } for (var i = equalPrefix; i < toState.length; ++i) { var enter = this.states[toState[i]].enter; if (enter) { obj.fsmStateName = toState[i]; if (enter.apply(obj)) return; } } obj.fsmStateName = nextStateName; } Engine.RegisterGlobal("FSM", FSM); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 8995) @@ -1,173 +1,174 @@ /** * Used to create player entities prior to reading the rest of a map, * all other initialization must be done after loading map (terrain/entities). * DO NOT use other components here, as they may fail unpredictably. */ function LoadPlayerSettings(settings) { // Default settings if (!settings) settings = {}; // Get default player data var rawData = Engine.ReadJSONFile("player_defaults.json"); if (!(rawData && rawData.PlayerData)) error("Error reading player default data: player_defaults.json"); var playerDefaults = rawData.PlayerData; // default number of players var numPlayers = 8; if (settings.PlayerData) { //Get number of players including gaia numPlayers = settings.PlayerData.length + 1; } else { warn("Setup has no player data - using defaults"); } // Get player manager var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var teams = []; var diplomacy = []; // Build team + diplomacy data for (var i = 0; i < (numPlayers - 1); ++i) { diplomacy[i] = cmpPlayerMan.Diplomacy.ENEMY; var pData = settings.PlayerData ? settings.PlayerData[i] : {}; var pDefs = playerDefaults ? playerDefaults[i+1] : {}; var team = getSetting(pData, pDefs, "Team"); // If team defined, add player to the team if (team !== undefined && team != -1) { if (!teams[team]) teams[team] = [i]; else teams[team].push(i); } } - + for (var i = 0; i < numPlayers; ++i) { // Add player entity to engine var entID = Engine.AddEntity("special/player"); // Retrieve entity var player = Engine.QueryInterface(entID, IID_Player); if (!player) error("Error creating new player entity in Setup.js!"); player.SetPlayerID(i); var pDefs = playerDefaults ? playerDefaults[i] : {}; // Use real player data if available if (i > 0) { var pData = settings.PlayerData ? settings.PlayerData[i-1] : {}; // Copy player data if not gaia player.SetName(getSetting(pData, pDefs, "Name")); player.SetCiv(getSetting(pData, pDefs, "Civ")); var colour = getSetting(pData, pDefs, "Colour"); player.SetColour(colour.r, colour.g, colour.b); if (getSetting(pData, pDefs, "PopulationLimit") !== undefined) player.SetPopulationLimit(getSetting(pData, pDefs, "PopulationLimit")); if (getSetting(pData, pDefs, "Resources") !== undefined) player.SetResourceCounts(getSetting(pData, pDefs, "Resources")); var team = getSetting(pData, pDefs, "Team"); //If diplomacy array exists use that, otherwise use team data or default diplomacy if (getSetting(pData, pDefs, "Diplomacy") !== undefined) { player.SetDiplomacy(getSetting(pData, pDefs, "Diplomacy")); } else if (team !== undefined && team != -1) { //Team exists var teamDiplomacy = diplomacy; for (var n in teams[team]) teamDiplomacy[n] = cmpPlayerMan.Diplomacy.ALLY; //Set ally player.SetDiplomacy(teamDiplomacy); } else { //Set default player.SetDiplomacy(diplomacy); } } else { // Copy gaia data from defaults player.SetName(pDefs.Name); player.SetCiv(pDefs.Civ); player.SetColour(pDefs.Colour.r, pDefs.Colour.g, pDefs.Colour.b); + player.SetDiplomacy(diplomacy); } // Add player to player manager cmpPlayerMan.AddPlayer(entID); } } // Get a setting if it exists or return default function getSetting(settings, defaults, property) { if (settings && (property in settings)) return settings[property]; // Use defaults if (defaults && (property in defaults)) return defaults[property]; return undefined; } /** * Similar to Engine.QueryInterface but applies to the player entity * that owns the given entity. * iid is typically IID_Player. */ function QueryOwnerInterface(ent, iid) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; var playerEnt = cmpPlayerMan.GetPlayerByID(cmpOwnership.GetOwner()); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } /** * Similar to Engine.QueryInterface but applies to the player entity * with the given ID number. * iid is typically IID_Player. */ function QueryPlayerIDInterface(id, iid) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerEnt = cmpPlayerMan.GetPlayerByID(id); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 8995) @@ -1,180 +1,176 @@ function Foundation() {} Foundation.prototype.Schema = ""; Foundation.prototype.Init = function() { // Foundations are initially 'uncommitted' and do not block unit movement at all // (to prevent players exploiting free foundations to confuse enemy units). // The first builder to reach the uncommitted foundation will tell friendly units // and animals to move out of the way, then will commit the foundation and enable // its obstruction once there's nothing in the way. this.committed = false; 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; this.initialised = true; }; 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.initialised) // this happens if the foundation was destroyed because the player had insufficient resources return; 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 number of seconds of construction work. * Returns true if the construction is completed. */ Foundation.prototype.Build = function(builderEnt, work) { // Do nothing if we've already finished building // (The entity will be destroyed soon after completion so // this won't happen much) if (this.buildProgress == 1.0) return; // Handle the initial 'committing' of the foundation if (!this.committed) { var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) { // If there's any units in the way, ask them to move away // and return early from this method. // Otherwise enable this obstruction so it blocks any further // units, and continue building. var collisions = cmpObstruction.GetConstructionCollisions(); if (collisions.length) { for each (var ent in collisions) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.LeaveFoundation(this.entity); - var cmpAnimalAI = Engine.QueryInterface(ent, IID_AnimalAI); - if (cmpAnimalAI) - cmpAnimalAI.LeaveFoundation(this.entity); - - // TODO: What if an obstruction has no UnitAI/AnimalAI? + // TODO: What if an obstruction has no UnitAI? } // TODO: maybe we should tell the builder to use a special // animation to indicate they're waiting for people to get // out the way return; } // The obstruction always blocks new foundations/construction, // but we've temporarily allowed units to walk all over it // (via CCmpTemplateManager). Now we need to remove that temporary // blocker-disabling, so that we'll perform standard unit blocking instead. cmpObstruction.SetDisableBlockMovementPathfinding(false); } this.committed = true; } // Calculate the amount of progress that will be added (where 1.0 = completion) var cmpCost = Engine.QueryInterface(this.entity, IID_Cost); var amount = work / cmpCost.GetBuildTime(); // 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 cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(); var cmpIdentity = Engine.QueryInterface(building, IID_Identity); if (cmpIdentity.GetClassesList().indexOf("CivCentre") != -1) cmpPlayerStatisticsTracker.IncreaseBuiltCivCentresCounter(); var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health); cmpBuildingHealth.SetHitpoints(cmpHealth.GetHitpoints()); Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": building }); Engine.DestroyEntity(this.entity); } }; Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 8995) @@ -1,1618 +1,1914 @@ function UnitAI() {} UnitAI.prototype.Schema = "Controls the unit's movement, attacks, etc, in response to commands from the player." + "" + "" + "" + - ""; + "" + + "" + + "" + + "" + + "" + + "violent" + + "aggressive" + + "defensive" + + "passive" + + "skittish" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""+ + "" + + ""; // Very basic stance support (currently just for test maps where we don't want // everyone killing each other immediately after loading) var g_Stances = { "aggressive": { attackOnSight: true, }, "holdfire": { attackOnSight: false, }, }; +// See ../helpers/FSM.js for some documentation of this FSM specification syntax var UnitFsmSpec = { // Default event handlers: "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 }, "ConstructionFinished": function(msg) { // ignore uninteresting construction messages }, "LosRangeUpdate": function(msg) { // ignore newly-seen units by default }, "Attacked": function(msg) { // ignore attacker }, + "HealthChanged": function(msg) { + // ignore + }, // Formation handlers: "FormationLeave": function(msg) { // ignore when we're not in FORMATIONMEMBER }, // Called when being told to walk as part of a formation "Order.FormationWalk": function(msg) { + if (this.IsAnimal()) + { + // TODO: let players move captured animals around + this.FinishOrder(); + return; + } + var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z); this.SetNextState("FORMATIONMEMBER.WALKING"); }, // Special orders: // (these will be overridden by various states) "Order.LeaveFoundation": function(msg) { // Default behaviour is to ignore the order since we're busy this.FinishOrder(); }, // Individual orders: // (these will switch the unit out of formation mode) "Order.Walk": function(msg) { + if (this.IsAnimal()) + { + // TODO: let players move captured animals around + this.FinishOrder(); + return; + } + this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("INDIVIDUAL.WALKING"); }, "Order.WalkToTarget": function(msg) { + if (this.IsAnimal()) + { + // TODO: let players move captured animals around + this.FinishOrder(); + return; + } + var ok = this.MoveToTarget(this.order.data.target); if (ok) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, "Order.Attack": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Work out how to attack the given target var type = this.GetBestAttack(); if (!type) { // Oops, we can't attack at all this.FinishOrder(); return; } this.attackType = type; // Try to move within attack range if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType)) { // We've started walking to the given point - this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); + if (this.IsAnimal()) + this.SetNextState("ANIMAL.COMBAT.APPROACHING"); + else + this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try attacking it from here. // TODO: need better handling of the can't-reach-target case - this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); + if (this.IsAnimal()) + this.SetNextState("ANIMAL.COMBAT.ATTACKING"); + else + this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); } }, "Order.Gather": function(msg) { // If the target is still alive, we need to kill it first if (this.MustKillGatherTarget(this.order.data.target)) { // Make sure we can attack the target, else we'll get very stuck if (!this.GetBestAttack()) { // Oops, we can't attack at all - give up // TODO: should do something so the player knows why this failed this.FinishOrder(); return; } this.PushOrderFront("Attack", { "target": this.order.data.target }); return; } // Try to move within range if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.GATHER.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try gathering it from here. // TODO: need better handling of the can't-reach-target case this.SetNextState("INDIVIDUAL.GATHER.GATHERING"); } }, "Order.ReturnResource": function(msg) { // Try to move to the dropsite if (this.MoveToTarget(this.order.data.target)) { // We've started walking to the target this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING"); } else { // Oops, we can't reach the dropsite. // Maybe we should try to pick another dropsite, to find an // accessible one? // For now, just give up. this.FinishOrder(); return; } }, "Order.Repair": function(msg) { // Try to move within range if (this.MoveToTargetRange(this.order.data.target, IID_Builder)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try repairing it from here. // TODO: need better handling of the can't-reach-target case this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING"); } }, "Order.Garrison": function(msg) { if (this.MoveToTarget(this.order.data.target)) { this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); } else { this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED"); } }, // States for the special entity representing a group of units moving in formation: "FORMATIONCONTROLLER": { "Order.Walk": function(msg) { this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKING"); }, "Order.Attack": function(msg) { // TODO: we should move in formation towards the target, // then break up into individuals when close enough to it var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("Attack", [msg.data.target, false]); // TODO: we should wait until the target is killed, then // move on to the next queued order. // Don't bother now, just disband the formation immediately. cmpFormation.Disband(); }, "Order.Repair": function(msg) { // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("Repair", [msg.data.target, false]); cmpFormation.Disband(); }, "Order.Gather": function(msg) { // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]); cmpFormation.Disband(); }, "Order.ReturnResource": function(msg) { // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("ReturnResource", [msg.data.target, false]); cmpFormation.Disband(); }, "Order.Garrison": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]); cmpFormation.Disband(); }, "IDLE": { }, "WALKING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.MoveMembersIntoFormation(true); }, "MoveCompleted": function(msg) { if (this.FinishOrder()) return; var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.Disband(); }, }, }, // States for entities moving as part of a formation: "FORMATIONMEMBER": { "FormationLeave": function(msg) { // Stop moving as soon as the formation disbands this.StopMoving(); // We're leaving the formation, so stop our FormationWalk order if (this.FinishOrder()) return; // No orders left, we're an individual now this.SetNextState("INDIVIDUAL.IDLE"); }, "IDLE": { "enter": function() { this.SelectAnimation("idle"); }, }, "WALKING": { "enter": function () { this.SelectAnimation("move"); }, }, }, // States for entities not part of a formation: "INDIVIDUAL": { + "enter": function() { + // Sanity-checking + if (this.IsAnimal()) + error("Animal got moved into INDIVIDUAL.* state"); + }, + "Attacked": function(msg) { // Default behaviour: attack back at our attacker if (this.CanAttack(msg.data.attacker)) { this.PushOrderFront("Attack", { "target": msg.data.attacker }); } else { // TODO: If unit can't attack, run away } }, "IDLE": { "enter": function() { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation this.SelectAnimation("idle"); // The GUI and AI want to know when a unit is idle, but we don't // want to send frequent spurious messages if the unit's only // idle for an instant and will quickly go off and do something else. // So we'll set a timer here and only report the idle event if we // remain idle this.StartTimer(1000); // If we entered the idle state we must have nothing better to do, // so immediately check whether there's anybody nearby to attack. // (If anyone approaches later, it'll be handled via LosRangeUpdate.) if (this.losRangeQuery) { var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var ents = rangeMan.ResetActiveQuery(this.losRangeQuery); if (this.GetStance().attackOnSight && this.AttackVisibleEntity(ents)) return true; // (abort the transition since we may have already switched state) } // Nobody to attack - stay in idle return false; }, "leave": function() { var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); rangeMan.DisableActiveQuery(this.losRangeQuery); this.StopTimer(); if (this.isIdle) { this.isIdle = false; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, "LosRangeUpdate": function(msg) { if (this.GetStance().attackOnSight) { // Start attacking one of the newly-seen enemy (if any) this.AttackVisibleEntity(msg.data.added); } }, "Timer": function(msg) { if (!this.isIdle) { this.isIdle = true; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, // Override the LeaveFoundation order since we're not doing // anything more important "Order.LeaveFoundation": function(msg) { // Move a tile outside the building var range = 4; var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range); if (ok) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, }, "WALKING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.FinishOrder(); }, }, "COMBAT": { "Attacked": function(msg) { // If we're already in combat mode, ignore anyone else // who's attacking us }, "APPROACHING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("ATTACKING"); }, }, "ATTACKING": { "enter": function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.attackTimers = cmpAttack.GetTimers(this.attackType); this.SelectAnimation("melee", false, 1.0, "attack"); this.SetAnimationSync(this.attackTimers.prepare, this.attackTimers.repeat); this.StartTimer(this.attackTimers.prepare, this.attackTimers.repeat); // TODO: we should probably only bother syncing projectile attacks, not melee // TODO: if .prepare is short, players can cheat by cycling attack/stop/attack // to beat the .repeat time; should enforce a minimum time }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { // Check the target is still alive if (this.TargetIsAlive(this.order.data.target)) { // Check we can still reach the target if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType)) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); cmpAttack.PerformAttack(this.attackType, this.order.data.target); return; } // Can't reach it - try to chase after it if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType)) { this.SetNextState("COMBAT.CHASING"); return; } } // Can't reach it, or it doesn't exist any more - give up this.FinishOrder(); - + // TODO: see if we can switch to a new nearby enemy }, // TODO: respond to target deaths immediately, rather than waiting // until the next Timer event }, "CHASING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("ATTACKING"); }, }, }, "GATHER": { "APPROACHING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function(msg) { if (msg.data.error) { // We failed to reach the target // Save the current order's data in case we need it later var oldType = this.order.data.type; var oldTarget = this.order.data.target; // Try the next queued order if there is any if (this.FinishOrder()) return; // Try to find another nearby target of the same specific type var nearby = this.FindNearbyResource(function (ent, type) { return (ent != oldTarget && type.specific == oldType.specific); }); if (nearby) { this.Gather(nearby, true); return; } // Couldn't find anything else. Just try this one again, // maybe we'll succeed next time this.Gather(oldTarget, true); return; } // We reached the target - start gathering from it now this.SetNextState("GATHERING"); }, }, "GATHERING": { "enter": function() { this.StartTimer(1000, 1000); // We want to start the gather animation as soon as possible, // but only if we're actually at the target and it's still alive // (else it'll look like we're chopping empty air). // (If it's not alive, the Timer handler will deal with sending us // off to a different target.) if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) { var typename = "gather_" + this.order.data.type.specific; this.SelectAnimation(typename, false, 1.0, typename); } }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { // Check we can still reach the target if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) { // Gather the resources: var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); // Try to gather treasure if (cmpResourceGatherer.TryInstantGather(this.order.data.target)) return; // If we've already got some resources but they're the wrong type, // drop them first to ensure we're only ever carrying one type if (cmpResourceGatherer.IsCarryingAnythingExcept(this.order.data.type.generic)) cmpResourceGatherer.DropResources(); // Collect from the target var status = cmpResourceGatherer.PerformGather(this.order.data.target); // TODO: if exhausted, we should probably stop immediately // and choose a new target // If we've collected as many resources as possible, // return to the nearest dropsite if (status.filled) { var nearby = this.FindNearestDropsite(this.order.data.type.generic); if (nearby) { // (Keep this Gather order on the stack so we'll // continue gathering after returning) this.PushOrderFront("ReturnResource", { "target": nearby }); return; } // Oh no, couldn't find any drop sites. Give up on gathering. this.FinishOrder(); } } else { // Try to follow the target if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) { this.SetNextState("APPROACHING"); return; } // Can't reach the target, or it doesn't exist any more // We want to carry on gathering resources in the same area as // the old one. So try to get close to the old resource's // last known position var maxRange = 8; // get close but not too close if (this.order.data.lastPos && this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z, 0, maxRange)) { this.SetNextState("APPROACHING"); return; } // We're already in range, or can't get anywhere near it. // Save the current order's type in case we need it later var oldType = this.order.data.type; // Give up on this order and try our next queued order if (this.FinishOrder()) return; // No remaining orders - pick a useful default behaviour // Try to find a new resource of the same specific type near our current position: var nearby = this.FindNearbyResource(function (ent, type) { return (type.specific == oldType.specific); }); if (nearby) { this.Gather(nearby, true); return; } // Nothing else to gather - if we're carrying anything then we should // drop it off, and if not then we might as well head to the dropsite // anyway because that's a nice enough place to congregate and idle var nearby = this.FindNearestDropsite(oldType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby }); return; } // No dropsites - just give up } }, }, }, // Returning to dropsite "RETURNRESOURCE": { "APPROACHING": { "enter": function () { // Work out what we're carrying, in order to select an appropriate animation var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var type = cmpResourceGatherer.GetLastCarriedType(); if (type) { var typename = "carry_" + type.generic; // Special case for meat if (type.specific == "meat") typename = "carry_" + type.specific; this.SelectAnimation(typename, false, this.GetWalkSpeed()); } else { // We're returning empty-handed this.SelectAnimation("move"); } }, "MoveCompleted": function() { // Switch back to idle animation to guarantee we won't // get stuck with the carry animation after stopping moving this.SelectAnimation("idle"); // Check the dropsite really is in range // (we didn't get stopped before reaching it) if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) { var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite); if (cmpResourceDropsite) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); cmpResourceGatherer.CommitResources(dropsiteTypes); // Our next order should always be a Gather, // so just switch back to that order this.FinishOrder(); return; } } // The dropsite was destroyed, or we couldn't reach it. // Look for a new one. var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var genericType = cmpResourceGatherer.GetMainCarryingType(); var nearby = this.FindNearestDropsite(genericType); if (nearby) { this.FinishOrder(); this.PushOrderFront("ReturnResource", { "target": nearby }); return; } // Oh no, couldn't find any drop sites. Give up on returning. this.FinishOrder(); }, }, }, "REPAIR": { "APPROACHING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("REPAIRING"); }, }, "REPAIRING": { "enter": function() { this.SelectAnimation("build", false, 1.0, "build"); this.StartTimer(1000, 1000); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { var target = this.order.data.target; // Check we can still reach the target if (!this.CheckTargetRange(target, IID_Builder)) { // Can't reach it, or it doesn't exist any more this.FinishOrder(); return; } var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); cmpBuilder.PerformBuilding(target); }, }, "ConstructionFinished": function(msg) { if (msg.data.entity != this.order.data.target) return; // ignore other buildings // Save the current order's data in case we need it later var oldAutocontinue = this.order.data.autocontinue; // We finished building it. // Switch to the next order (if any) if (this.FinishOrder()) return; // No remaining orders - pick a useful default behaviour // If autocontinue explicitly disabled (e.g. by AI) then // do nothing automatically if (!oldAutocontinue) return; // If this building was e.g. a farm, we should start gathering from it // if we are capable of doing so if (this.CanGather(msg.data.newentity)) { this.Gather(msg.data.newentity, true); return; } // If this building was e.g. a farmstead, we should look for nearby // resources we can gather var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); if (cmpResourceDropsite) { var types = cmpResourceDropsite.GetTypes(); var nearby = this.FindNearbyResource(function (ent, type) { return (types.indexOf(type.generic) != -1); }); if (nearby) { this.Gather(nearby, true); return; } } // TODO: look for a nearby foundation to help with }, // Override the LeaveFoundation order since we don't want to be // accidentally blocking our own building "Order.LeaveFoundation": function(msg) { // Move a tile outside the building var range = 4; var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range); if (ok) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, }, "GARRISON": { "APPROACHING": { "enter": function() { this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.PlaySound("walk"); }, "MoveCompleted": function() { this.SetNextState("GARRISONED"); }, "leave": function() { this.StopTimer(); } }, "GARRISONED": { "enter": function() { var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder); if (cmpGarrisonHolder) { cmpGarrisonHolder.Garrison(this.entity); } if (this.FinishOrder()) return; }, "leave": function() { } }, }, }, + + "ANIMAL": { + + "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"); + }, + + "Attacked": function(msg) { + if (this.template.NaturalBehaviour == "skittish" || + this.template.NaturalBehaviour == "passive") + { + this.MoveToTargetRangeExplicit(msg.data.attacker, +this.template.FleeDistance, +this.template.FleeDistance); + this.SetNextState("FLEEING"); + this.PlaySound("panic"); + } + else if (this.template.NaturalBehaviour == "violent" || + this.template.NaturalBehaviour == "aggressive" || + this.template.NaturalBehaviour == "defensive") + { + if (this.CanAttack(msg.data.attacker)) + this.ReplaceOrder("Attack", { "target": msg.data.attacker }); + } + }, + + "Order.LeaveFoundation": function(msg) { + // Run away from the foundation + this.MoveToTargetRangeExplicit(msg.data.target, +this.template.FleeDistance, +this.template.FleeDistance); + this.SetNextState("FLEEING"); + this.PlaySound("panic"); + }, + + "IDLE": { + // (We need an IDLE state so that FinishOrder works) + + "enter": function() { + // Start feeding immediately + this.SetNextState("FEEDING"); + return true; + }, + }, + + "CORPSE": { + "enter": function() { + this.StopMoving(); + }, + + // Ignore all orders that animals might otherwise respond to + "Order.FormationWalk": function() { }, + "Order.Walk": function() { }, + "Order.WalkToTarget": function() { }, + "Order.Attack": function() { }, + + "Attacked": function(msg) { + // Do nothing, because we're dead already + }, + + "Order.LeaveFoundation": function(msg) { + // We can't walk away from the foundation (since we're dead), + // but we mustn't block its construction (since the builders would get stuck), + // and we don't want to trick gatherers into trying to reach us when + // we're stuck in the middle of a building, so just delete our corpse. + Engine.DestroyEntity(this.entity); + }, + }, + + "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(); + }, + + "LosRangeUpdate": function(msg) { + if (this.template.NaturalBehaviour == "skittish") + { + if (msg.data.added.length > 0) + { + this.MoveToTargetRangeExplicit(msg.data.added[0], +this.template.FleeDistance, +this.template.FleeDistance); + this.SetNextState("FLEEING"); + this.PlaySound("panic"); + return; + } + } + // Start attacking one of the newly-seen enemy (if any) + else if (this.template.NaturalBehaviour == "violent" || + this.template.NaturalBehaviour == "aggressive") + { + this.AttackVisibleEntity(msg.data.added); + } + + // TODO: if two units enter our range together, we'll attack the + // first and then the second won't trigger another LosRangeUpdate + // so we won't notice it. Probably we should do something with + // ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to + // find any units that are already in range. + }, + + "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(); + }, + + "LosRangeUpdate": function(msg) { + if (this.template.NaturalBehaviour == "skittish") + { + if (msg.data.added.length > 0) + { + this.MoveToTargetRangeExplicit(msg.data.added[0], +this.template.FleeDistance, +this.template.FleeDistance); + this.SetNextState("FLEEING"); + this.PlaySound("panic"); + return; + } + } + // Start attacking one of the newly-seen enemy (if any) + else if (this.template.NaturalBehaviour == "violent") + { + this.AttackVisibleEntity(msg.data.added); + } + }, + + "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"); + }, + }, + + "COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals + }, }; var UnitFsm = new FSM(UnitFsmSpec); UnitAI.prototype.Init = function() { this.orderQueue = []; // current order is at the front of the list this.order = undefined; // always == this.orderQueue[0] this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to this.isIdle = false; this.SetStance("aggressive"); }; UnitAI.prototype.IsFormationController = function() { return (this.template.FormationController == "true"); }; +UnitAI.prototype.IsAnimal = function() +{ + return (this.template.NaturalBehaviour ? true : false); +}; + UnitAI.prototype.IsIdle = function() { return this.isIdle; }; UnitAI.prototype.OnCreate = function() { - if (this.IsFormationController()) + if (this.IsAnimal()) + UnitFsm.Init(this, "ANIMAL.FEEDING"); + else if (this.IsFormationController()) UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE"); else UnitFsm.Init(this, "INDIVIDUAL.IDLE"); }; UnitAI.prototype.OnOwnershipChanged = function(msg) { this.SetupRangeQuery(msg.to); }; UnitAI.prototype.OnDestroy = function() { // Clean up any timers that are now obsolete this.StopTimer(); // Clean up range queries var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) rangeMan.DestroyActiveQuery(this.losRangeQuery); }; // Set up a range query for all enemy units within LOS range // which can be attacked. // This should be called whenever our ownership changes. UnitAI.prototype.SetupRangeQuery = function(owner) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.losRangeQuery) rangeMan.DestroyActiveQuery(this.losRangeQuery); var range = cmpVision.GetRange(); var players = []; if (owner != -1) { // If unit not just killed, get enemy players via diplomacy var player = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); // Get our diplomacy array var diplomacy = player.GetDiplomacy(); var numPlayers = playerMan.GetNumPlayers(); - + for (var i = 1; i < numPlayers; ++i) { // Exclude gaia, allies, and self // TODO: How to handle neutral players - Special query to attack military only? if (i != owner && diplomacy[i - 1] < 0) players.push(i); } } this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, 0, range, players, IID_DamageReceiver); rangeMan.EnableActiveQuery(this.losRangeQuery); }; //// FSM linkage functions //// UnitAI.prototype.SetNextState = function(state) { UnitFsm.SetNextState(this, state); }; UnitAI.prototype.DeferMessage = function(msg) { UnitFsm.DeferMessage(this, msg); }; /** * Call when the current order has been completed (or failed). * Removes the current order from the queue, and processes the * next one (if any). Returns false and defaults to IDLE * if there are no remaining orders. */ UnitAI.prototype.FinishOrder = function() { if (!this.orderQueue.length) error("FinishOrder called when order queue is empty"); this.orderQueue.shift(); this.order = this.orderQueue[0]; if (this.orderQueue.length) { UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data}); return true; } else { this.SetNextState("IDLE"); return false; } }; /** * Add an order onto the back of the queue, * and execute it if we didn't already have an order. */ UnitAI.prototype.PushOrder = function(type, data) { var order = { "type": type, "data": data }; this.orderQueue.push(order); // If we didn't already have an order, then process this new one if (this.orderQueue.length == 1) { this.order = order; UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data}); } }; /** * Add an order onto the front of the queue, * and execute it immediately. */ UnitAI.prototype.PushOrderFront = function(type, data) { var order = { "type": type, "data": data }; this.orderQueue.unshift(order); this.order = order; UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data}); }; UnitAI.prototype.ReplaceOrder = function(type, data) { this.orderQueue = []; this.PushOrder(type, data); }; UnitAI.prototype.TimerHandler = function(data, lateness) { // Reset the timer if (data.timerRepeat === undefined) { this.timer = undefined; } else { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", data.timerRepeat - lateness, data); } UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness}); }; /** * Set up the UnitAI timer to run after 'offset' msecs, and then * every 'repeat' msecs until StopTimer is called. A "Timer" message * will be sent each time the timer runs. */ UnitAI.prototype.StartTimer = function(offset, repeat) { if (this.timer) error("Called StartTimer when there's already an active timer"); var data = { "timerRepeat": repeat }; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data); }; /** * Stop the current UnitAI timer. */ UnitAI.prototype.StopTimer = function() { if (!this.timer) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; //// Message handlers ///// UnitAI.prototype.OnMotionChanged = function(msg) { if (msg.starting && !msg.error) { UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); } else if (!msg.starting || msg.error) { UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); } }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) { // TODO: This is a bit inefficient since every unit listens to every // construction message - ideally we could scope it to only the one we're building UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg}); }; UnitAI.prototype.OnAttacked = function(msg) { UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg}); }; +UnitAI.prototype.OnHealthChanged = function(msg) +{ + UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to}); +}; + UnitAI.prototype.OnRangeUpdate = function(msg) { if (msg.tag == this.losRangeQuery) UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg}); }; //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.GetWalkSpeed(); }; UnitAI.prototype.GetRunSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.GetRunSpeed(); }; /** * Returns true if the target exists and has non-zero hitpoints. */ UnitAI.prototype.TargetIsAlive = function(ent) { var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (!cmpHealth) return false; return (cmpHealth.GetHitpoints() != 0); }; /** * Returns true if the target exists and needs to be killed before * beginning to gather resources from it. */ UnitAI.prototype.MustKillGatherTarget = function(ent) { var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (!cmpResourceSupply) return false; if (!cmpResourceSupply.GetKillBeforeGather()) return false; return this.TargetIsAlive(ent); }; /** * Returns the entity ID of the nearest resource supply where the given * filter returns true, or undefined if none can be found. * TODO: extend this to exclude resources that already have lots of * gatherers. */ UnitAI.prototype.FindNearbyResource = function(filter) { var range = 64; // TODO: what's a sensible number? // Accept any resources owned by Gaia var players = [0]; // Also accept resources owned by this unit's player: var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) players.push(cmpOwnership.GetOwner()); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply); for each (var ent in nearby) { var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); var type = cmpResourceSupply.GetType(); if (filter(ent, type)) return ent; } return undefined; }; /** * Returns the entity ID of the nearest resource dropsite that accepts * the given type, or undefined if none can be found. */ UnitAI.prototype.FindNearestDropsite = function(genericType) { // Find dropsites owned by this unit's player var players = []; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) players.push(cmpOwnership.GetOwner()); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite); for each (var ent in nearby) { var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (!cmpDropsite.AcceptsType(genericType)) continue; return ent; } return undefined; }; /** * Play a sound appropriate to the current entity. */ UnitAI.prototype.PlaySound = function(name) { // If we're a formation controller, use the sounds from our first member if (this.IsFormationController()) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); var member = cmpFormation.GetPrimaryMember(); if (member) PlaySound(name, member); } else { // Otherwise use our own sounds PlaySound(name, this.entity); } }; UnitAI.prototype.SelectAnimation = function(name, once, speed, sound) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; // Special case: the "move" animation gets turned into a special // movement mode that deals with speeds and walk/run automatically if (name == "move") { // Speed to switch from walking to running animations var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2; cmpVisual.SelectMovementAnimation(runThreshold); 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); }; UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetAnimationSyncRepeat(repeattime); cmpVisual.SetAnimationSyncOffset(actiontime); }; UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.StopMoving(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; UnitAI.prototype.MoveToTarget = function(target) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, 0, 0); }; UnitAI.prototype.MoveToTargetRange = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, min, max); }; UnitAI.prototype.CheckTargetRange = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); }; UnitAI.prototype.GetBestAttack = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; return cmpAttack.GetBestAttack(); }; /** * Try to find one of the given entities which can be attacked, * and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackVisibleEntity = function(ents) { for each (var target in ents) { if (this.CanAttack(target)) { this.PushOrderFront("Attack", { "target": target }); return true; } } return false; }; //// External interface functions //// UnitAI.prototype.SetFormationController = function(ent) { this.formationController = ent; // Set obstruction group, so we can walk through members // of our own formation (or ourself if not in formation) var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) { if (ent == INVALID_ENTITY) cmpObstruction.SetControlGroup(this.entity); else cmpObstruction.SetControlGroup(ent); } // If we were removed from a formation, let the FSM switch back to INDIVIDUAL if (ent == INVALID_ENTITY) UnitFsm.ProcessMessage(this, { "type": "FormationLeave" }); }; UnitAI.prototype.GetFormationController = function() { return this.formationController; }; /** * Returns the estimated distance that this unit will travel before either * finishing all of its orders, or reaching a non-walk target (attack, gather, etc). * Intended for Formation to switch to column layout on long walks. */ UnitAI.prototype.ComputeWalkingDistance = function() { var distance = 0; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return 0; // Keep track of the position at the start of each order var pos = cmpPosition.GetPosition(); for (var i = 0; i < this.orderQueue.length; ++i) { var order = this.orderQueue[i]; switch (order.type) { case "Walk": // Add the distance to the target point var dx = order.data.x - pos.x; var dz = order.data.z - pos.z; var d = Math.sqrt(dx*dx + dz*dz); distance += d; // Remember this as the start position for the next order pos = order.data; break; // and continue the loop case "WalkToTarget": case "LeaveFoundation": case "Attack": case "Gather": case "ReturnResource": case "Repair": case "Garrison": // Find the target unit's position var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return distance; var targetPos = cmpTargetPosition.GetPosition(); // Add the distance to the target unit var dx = targetPos.x - pos.x; var dz = targetPos.z - pos.z; var d = Math.sqrt(dx*dx + dz*dz); distance += d; // Return the total distance to the target return distance; default: error("ComputeWalkingDistance: Unrecognised order type '"+order.type+"'"); return distance; } } // Return the total distance to the end of the order queue return distance; }; UnitAI.prototype.AddOrder = function(type, data, queued) { if (queued) this.PushOrder(type, data); else this.ReplaceOrder(type, data); }; UnitAI.prototype.Walk = function(x, z, queued) { this.AddOrder("Walk", { "x": x, "z": z }, queued); }; UnitAI.prototype.WalkToTarget = function(target, queued) { this.AddOrder("WalkToTarget", { "target": target }, queued); }; UnitAI.prototype.LeaveFoundation = function(target) { // TODO: we should verify this is a friendly foundation, otherwise // there's no reason we should let them build here this.PushOrderFront("LeaveFoundation", { "target": target }); }; UnitAI.prototype.Attack = function(target, queued) { if (!this.CanAttack(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Attack", { "target": target }, queued); }; UnitAI.prototype.Garrison = function(target, queued) { if (!this.CanGarrison(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Garrison", { "target": target }, queued); }; UnitAI.prototype.Gather = function(target, queued) { if (!this.CanGather(target)) { this.WalkToTarget(target, queued); return; } // Save the resource type now, so if the resource gets destroyed // before we process the order then we still know what resource // type to look for more of var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); var type = cmpResourceSupply.GetType(); // Remember the position of our target, if any, in case it disappears // later and we want to head to its last known position // (TODO: if the target moves a lot (e.g. it's an animal), maybe we // need to update this lastPos regularly rather than just here?) var lastPos = undefined; var cmpPosition = Engine.QueryInterface(target, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) lastPos = cmpPosition.GetPosition(); this.AddOrder("Gather", { "target": target, "type": type, "lastPos": lastPos }, queued); }; UnitAI.prototype.ReturnResource = function(target, queued) { if (!this.CanReturnResource(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("ReturnResource", { "target": target }, queued); }; UnitAI.prototype.Repair = function(target, autocontinue, queued) { if (!this.CanRepair(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue }, queued); }; UnitAI.prototype.SetStance = function(stance) { if (g_Stances[stance]) this.stance = stance; else error("UnitAI: Setting to invalid stance '"+stance+"'"); }; UnitAI.prototype.GetStance = function() { return g_Stances[this.stance]; }; //// Helper functions //// UnitAI.prototype.CanAttack = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Attack commands var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; // TODO: verify that this is a valid target return true; }; UnitAI.prototype.CanGarrison = function(target) { var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; - + + // Don't let animals garrison for now + // (If we want to support that, we'll need to change Order.Garrison so it + // doesn't move the animal into an INVIDIDUAL.* state) + if (this.IsAnimal()) + return false; + return true; }; UnitAI.prototype.CanGather = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Gather commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; // Verify that we can gather from this target if (!cmpResourceGatherer.GetTargetGatherRate(target)) return false; // TODO: should verify it's owned by the correct player, etc return true; }; UnitAI.prototype.CanReturnResource = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to ReturnResource commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; // Verify that the target is a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return false; // Verify that we are carrying some resources, // and can return our current resource to this target var type = cmpResourceGatherer.GetMainCarryingType(); if (!type || !cmpResourceDropsite.AcceptsType(type)) return false; // TODO: should verify it's owned by the correct player, etc return true; }; UnitAI.prototype.CanRepair = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Repair (Builder) commands var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); if (!cmpBuilder) return false; // TODO: verify that this is a valid target return true; }; +//// Animal specific functions //// + +UnitAI.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); +}; + +UnitAI.prototype.SetMoveSpeed = function(speed) +{ + var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpMotion.SetSpeed(speed); +}; Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_defensive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_defensive.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_defensive.xml (revision 8995) @@ -1,8 +1,8 @@ - + defensive - + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive.xml (revision 8995) @@ -1,8 +1,17 @@ - + aggressive - + + + + 1.0 + 1.0 + 0.0 + 4.0 + 1000 + + Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 8995) @@ -1,45 +1,45 @@ gaia Chicken gaia/fauna_chicken.png 40 food.meat fauna/chicken.xml 5 3.0 1.5 - + 4.0 12.0 2000 8000 10000 40000 - + 1.0 6.0 actor/fauna/animal/chickens.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_whale_humpback.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_whale_humpback.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_whale_humpback.xml (revision 8995) @@ -1,52 +1,52 @@ gaia Whale Humpback Whale Collect food from this bountiful oceanic resource. gaia/fauna_fish.png fauna/whale.xml corpse 500 1 false false ship 2.0 6.0 2.5 -4.0 true - + skittish 20.0 40.0 10000 30000 1000 2000 - + true 2000 food.fish Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_violent.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_violent.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_violent.xml (revision 8995) @@ -1,8 +1,17 @@ - + violent - + + + + 1.0 + 1.0 + 0.0 + 4.0 + 1000 + + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_passive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_passive.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_passive.xml (revision 8995) @@ -1,8 +1,8 @@ - + passive - + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_breed_passive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_breed_passive.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_breed_passive.xml (revision 8995) @@ -1,8 +1,8 @@ - + passive - + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_passive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_passive.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_passive.xml (revision 8995) @@ -1,8 +1,8 @@ - + passive - + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_violent.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_violent.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_violent.xml (revision 8995) @@ -1,8 +1,17 @@ - + violent - + + + + 1.0 + 1.0 + 0.0 + 4.0 + 1000 + + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive.xml (revision 8995) @@ -1,8 +1,17 @@ - - violent - + + aggressive + + + + 1.0 + 1.0 + 0.0 + 4.0 + 1000 + + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_passive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_passive.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_passive.xml (revision 8995) @@ -1,8 +1,8 @@ - + passive - + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml (revision 8995) @@ -1,37 +1,36 @@ Fauna Animal Organic gaia/fauna_bear.png food 0 10 2.25 2.0 6.0 - - + 8.0 32.0 2000 8000 15000 60000 - + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_fish.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_fish.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_fish.xml (revision 8995) @@ -1,27 +1,27 @@ Fish 2.5 false false -4.0 true - + false 1000 food.fish Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish.xml (revision 8995) @@ -1,8 +1,8 @@ - + skittish - + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive.xml (revision 8994) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive.xml (revision 8995) @@ -1,8 +1,8 @@ - + defensive - +