Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 7321) +++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 7322) @@ -1,296 +1,311 @@ /* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpUnitMotion.h" #include "ICmpPosition.h" #include "ICmpPathfinder.h" #include "simulation2/MessageTypes.h" class CCmpUnitMotion : public ICmpUnitMotion { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Update); } DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) const CSimContext* m_Context; // Template state: CFixed_23_8 m_Speed; // in units per second // Dynamic state: bool m_HasTarget; ICmpPathfinder::Path m_Path; - entity_pos_t m_TargetX, m_TargetZ; // these values contain undefined junk if !HasTarget + // These values contain undefined junk if !HasTarget: + entity_pos_t m_TargetX, m_TargetZ; // currently-selected waypoint + entity_pos_t m_FinalTargetX, m_FinalTargetZ; // final target center (used to face towards it) enum { IDLE, WALKING, STOPPING }; int m_State; virtual void Init(const CSimContext& context, const CParamNode& paramNode) { m_Context = &context; m_HasTarget = false; m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); m_State = IDLE; } virtual void Deinit(const CSimContext& UNUSED(context)) { } virtual void Serialize(ISerializer& serialize) { serialize.Bool("has target", m_HasTarget); if (m_HasTarget) { // TODO: m_Path serialize.NumberFixed_Unbounded("target x", m_TargetX); serialize.NumberFixed_Unbounded("target z", m_TargetZ); + // TODO: m_FinalTargetAngle } // TODO: m_State } virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& deserialize) { Init(context, paramNode); deserialize.Bool(m_HasTarget); if (m_HasTarget) { deserialize.NumberFixed_Unbounded(m_TargetX); deserialize.NumberFixed_Unbounded(m_TargetZ); } } virtual void HandleMessage(const CSimContext& context, const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Update: { CFixed_23_8 dt = static_cast (msg).turnLength; if (m_State == STOPPING) { + m_State = IDLE; CMessageMotionChanged msg(CFixed_23_8::FromInt(0)); context.GetComponentManager().PostMessage(GetEntityId(), msg); - m_State = IDLE; } Move(context, dt); break; } } } void SwitchState(const CSimContext& context, int state) { debug_assert(state == IDLE || state == WALKING); // IDLE -> IDLE -- no change // IDLE -> WALKING -- send a MotionChanged message // WALKING -> IDLE -- set to STOPPING, so we'll send MotionChanged in the next Update // WALKING -> WALKING -- no change // STOPPING -> IDLE -- stay in STOPPING // STOPPING -> WALKING -- set to WALKING, send no messages if (m_State == IDLE && state == WALKING) { CMessageMotionChanged msg(m_Speed); context.GetComponentManager().PostMessage(GetEntityId(), msg); m_State = WALKING; return; } if (m_State == WALKING && state == IDLE) { m_State = STOPPING; return; } if (m_State == STOPPING && state == IDLE) { return; } if (m_State == STOPPING && state == WALKING) { m_State = WALKING; return; } } virtual void MoveToPoint(entity_pos_t x, entity_pos_t z, entity_pos_t minRadius, entity_pos_t maxRadius) { CmpPtr cmpPathfinder (*m_Context, SYSTEM_ENTITY); if (cmpPathfinder.null()) return; CmpPtr cmpPosition(*m_Context, GetEntityId()); if (cmpPosition.null()) return; SwitchState(*m_Context, WALKING); CFixedVector3D pos = cmpPosition->GetPosition(); m_Path.m_Waypoints.clear(); // u32 cost; // entity_pos_t r = entity_pos_t::FromInt(0); // TODO: should get this from the entity's size // if (cmpPathfinder->CanMoveStraight(pos.X, pos.Z, x, z, r, cost)) // { // m_TargetX = x; // m_TargetZ = z; // m_HasTarget = true; // } // else { ICmpPathfinder::Goal goal; goal.x = x; goal.z = z; goal.minRadius = minRadius; goal.maxRadius = maxRadius; cmpPathfinder->SetDebugPath(pos.X, pos.Z, goal); cmpPathfinder->ComputePath(pos.X, pos.Z, goal, m_Path); // If there's no waypoints then we've stopped already, otherwise move to the first one if (m_Path.m_Waypoints.empty()) { m_HasTarget = false; SwitchState(*m_Context, IDLE); } else { + m_FinalTargetX = x; + m_FinalTargetZ = z; PickNextWaypoint(pos); } } } virtual CFixed_23_8 GetSpeed() { return m_Speed; } void Move(const CSimContext& context, CFixed_23_8 dt); void PickNextWaypoint(const CFixedVector3D& pos); }; REGISTER_COMPONENT_TYPE(UnitMotion) void CCmpUnitMotion::Move(const CSimContext& context, CFixed_23_8 dt) { if (!m_HasTarget) return; CmpPtr cmpPosition(context, GetEntityId()); if (cmpPosition.null()) return; CFixedVector3D pos = cmpPosition->GetPosition(); pos.Y = CFixed_23_8::FromInt(0); // remove Y so it doesn't influence our distance calculations // We want to move (at most) m_Speed*dt units from pos towards the next waypoint while (dt > CFixed_23_8::FromInt(0)) { CFixedVector3D target(m_TargetX, CFixed_23_8::FromInt(0), m_TargetZ); CFixedVector3D offset = target - pos; // Face towards the target entity_angle_t angle = atan2_approx(offset.X, offset.Z); cmpPosition->TurnTo(angle); // Work out how far we can travel in dt CFixed_23_8 maxdist = m_Speed.Multiply(dt); // If the target is close, we can move there directly if (offset.Length() <= maxdist) { // If we've reached the last waypoint, stop if (m_Path.m_Waypoints.empty()) { cmpPosition->MoveTo(target.X, target.Z); + + // If we didn't reach the final goal, point towards it now + if (target.X != m_FinalTargetX || target.Z != m_FinalTargetZ) + { + CFixedVector3D final(m_FinalTargetX, CFixed_23_8::FromInt(0), m_FinalTargetZ); + CFixedVector3D finalOffset = final - target; + entity_angle_t angle = atan2_approx(finalOffset.X, finalOffset.Z); + cmpPosition->TurnTo(angle); + } + m_HasTarget = false; SwitchState(context, IDLE); return; } // Otherwise, spend the rest of the time heading towards the next waypoint dt = dt - (offset.Length() / m_Speed); pos = target; PickNextWaypoint(pos); continue; } else { // Not close enough, so just move in the right direction offset.Normalize(maxdist); pos += offset; cmpPosition->MoveTo(pos.X, pos.Z); return; } } } void CCmpUnitMotion::PickNextWaypoint(const CFixedVector3D& pos) { // We can always pick the immediate next waypoint debug_assert(!m_Path.m_Waypoints.empty()); m_TargetX = m_Path.m_Waypoints.back().x; m_TargetZ = m_Path.m_Waypoints.back().z; m_Path.m_Waypoints.pop_back(); m_HasTarget = true; // To smooth the motion and avoid grid-constrained motion, we could try picking some // subsequent waypoints instead, if we can reach them without hitting any obstacles CmpPtr cmpPathfinder (*m_Context, SYSTEM_ENTITY); if (cmpPathfinder.null()) return; for (size_t i = 0; i < 3 && !m_Path.m_Waypoints.empty(); ++i) { u32 cost; entity_pos_t r = entity_pos_t::FromInt(0); // TODO: should get this from the entity's size if (!cmpPathfinder->CanMoveStraight(pos.X, pos.Z, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z, r, cost)) break; m_TargetX = m_Path.m_Waypoints.back().x; m_TargetZ = m_Path.m_Waypoints.back().z; m_Path.m_Waypoints.pop_back(); } } Index: ps/trunk/source/maths/Fixed.h =================================================================== --- ps/trunk/source/maths/Fixed.h (revision 7321) +++ ps/trunk/source/maths/Fixed.h (revision 7322) @@ -1,165 +1,168 @@ /* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_FIXED #define INCLUDED_FIXED #include "lib/types.h" #include "maths/Sqrt.h" template inline T round_away_from_zero(float value) { return (T)(value >= 0 ? value + 0.5f : value - 0.5f); } /** * A simple fixed-point number class, with no fancy features * like overflow checking or anything. (It has very few basic * features too, and needs to be substantially improved before * it'll be of much use.) * * Use CFixed_23_8 rather than using this class directly. */ template class CFixed { private: T value; explicit CFixed(T v) : value(v) { } public: enum { fract_bits = fract_bits_ }; CFixed() : value(0) { } T GetInternalValue() const { return value; } void SetInternalValue(T n) { value = n; } // Conversion to/from primitive types: static CFixed FromInt(int n) { return CFixed(n << fract_bits); } static CFixed FromFloat(float n) { if (!isfinite(n)) return CFixed(0); float scaled = n * fract_pow2; return CFixed(round_away_from_zero(scaled)); } static CFixed FromDouble(double n) { if (!isfinite(n)) return CFixed(0); double scaled = n * fract_pow2; return CFixed(round_away_from_zero(scaled)); } float ToFloat() const { return value / (float)fract_pow2; } double ToDouble() const { return value / (double)fract_pow2; } int ToInt_RoundToZero() const { if (value > 0) return value >> fract_bits; else return (value + fract_pow2 - 1) >> fract_bits; } /// Returns true if the number is precisely 0. bool IsZero() const { return value == 0; } /// Equality. bool operator==(CFixed n) const { return (value == n.value); } + /// Inequality. + bool operator!=(CFixed n) const { return (value != n.value); } + /// Numeric comparison. bool operator<=(CFixed n) const { return (value <= n.value); } /// Numeric comparison. bool operator<(CFixed n) const { return (value < n.value); } /// Numeric comparison. bool operator>=(CFixed n) const { return (value >= n.value); } /// Numeric comparison. bool operator>(CFixed n) const { return (value > n.value); } // Basic arithmetic: /// Add a CFixed. Might overflow. CFixed operator+(CFixed n) const { return CFixed(value + n.value); } /// Subtract a CFixed. Might overflow. CFixed operator-(CFixed n) const { return CFixed(value - n.value); } /// Negate a CFixed. CFixed operator-() const { return CFixed(-value); } /// Divide by a CFixed. Must not have n.IsZero(). Might overflow. CFixed operator/(CFixed n) const { i64 t = (i64)value << fract_bits; return CFixed((T)(t / (i64)n.value)); } /// Multiply by an integer. Might overflow. CFixed operator*(int n) const { return CFixed(value * n); } /// Divide by an integer. Must not have n == 0. Cannot overflow. CFixed operator/(int n) const { return CFixed(value / n); } CFixed Absolute() const { return CFixed(abs(value)); } /** * Multiply by a CFixed. Likely to overflow if both numbers are large, * so we use an ugly name instead of operator* to make it obvious. */ CFixed Multiply(CFixed n) const { i64 t = (i64)value * (i64)n.value; return CFixed((T)(t >> fract_bits)); } CFixed Sqrt() const { if (value <= 0) return CFixed(0); u32 s = isqrt64(value); return CFixed((u64)s << (fract_bits / 2)); } }; /** * A fixed-point number class with 1-bit sign, 23-bit integral part, 8-bit fractional part. */ typedef CFixed CFixed_23_8; /** * Inaccurate approximation of atan2 over fixed-point numbers. * Maximum error is almost 0.08 radians (4.5 degrees). */ CFixed_23_8 atan2_approx(CFixed_23_8 y, CFixed_23_8 x); #endif // INCLUDED_FIXED Index: ps/trunk/binaries/data/mods/public/gui/session_new/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session_new/input.js (revision 7321) +++ ps/trunk/binaries/data/mods/public/gui/session_new/input.js (revision 7322) @@ -1,186 +1,204 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too var INPUT_NORMAL = 0; var INPUT_DRAGGING = 1; var INPUT_BUILDING_PLACEMENT = 2; 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") + if (action.cursor) { - Engine.SetCursor("action-" + action.type); + Engine.SetCursor(action.cursor); return; } } Engine.SetCursor("arrow-default"); } +function findGatherType(gatherer, supply) +{ + if (!gatherer || !supply) + return; + if (gatherer[supply.type.generic+"."+supply.type.specific]) + return supply.type.specific; + if (gatherer[supply.type.generic]) + return supply.type.generic; +} + /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ 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]}; + return {"type": "attack", "cursor": "action-attack", "target": targets[0]}; + + var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply); + if (resource) + return {"type": "gather", "cursor": "action-gather-"+resource, "target": targets[0]}; // TODO: need more actions // 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; } /* Selection methods: (not all currently implemented) - Left-click on entity to select (always chooses the 'closest' one if the mouse is over several). Includes non-controllable units (e.g. trees, enemy units). - Double-left-click to select entity plus all of the same type on the screen. - Triple-left-click to select entity plus all of the same type in the world. - Left-click-and-drag to select all in region. Only includes controllable units. - Left-click on empty space to deselect all. - Hotkeys to select various groups. - Shift plus left-click on entity to toggle selection of that unit. Only includes controllable. - Shift plus any other selection method above, to add them to current selection. */ function handleInputAfterGui(ev) { switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (!ents.length) { resetEntitySelection(); return true; } resetEntitySelection(); addEntitySelection([ents[0]]); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { 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": selection, "x": target.x, "z": target.z}); return true; case "attack": Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target}); return true; + + case "gather": + Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target}); + return true; } } } break; case INPUT_BUILDING_PLACEMENT: switch (ev.type) { case "mousemotion": var target = Engine.GetTerrainAtPoint(ev.x, ev.y); var angle = Math.PI; Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": placementEntity, "x": target.x, "z": target.z, "angle": angle}); return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { var target = Engine.GetTerrainAtPoint(ev.x, ev.y); var angle = Math.PI; Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""}); Engine.PostNetworkCommand({"type": "construct", "template": placementEntity, "x": target.x, "z": target.z, "angle": angle}); inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""}); inputState = INPUT_NORMAL; return true; } } break; } return false; } function testBuild(ent) { placementEntity = ent; inputState = INPUT_BUILDING_PLACEMENT; } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 7321) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 7322) @@ -1,46 +1,56 @@ function ProcessCommand(player, cmd) { // print("command: " + player + " " + uneval(cmd) + "\n"); switch (cmd.type) { case "walk": for each (var ent in cmd.entities) { var ai = Engine.QueryInterface(ent, IID_UnitAI); if (!ai) continue; 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; + case "gather": + for each (var ent in cmd.entities) + { + var ai = Engine.QueryInterface(ent, IID_UnitAI); + if (!ai) + continue; + ai.Gather(cmd.target); + } + break; + case "construct": // TODO: this should do all sorts of stuff with foundations and resource costs etc var ent = Engine.AddEntity(cmd.template); if (ent) { var pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } } break; default: print("Ignoring unrecognised command type '" + cmd.type + "'\n"); } } Engine.RegisterGlobal("ProcessCommand", ProcessCommand); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 7322) @@ -0,0 +1,42 @@ +function ResourceSupply() {} + +ResourceSupply.prototype.Init = function() +{ + // Current resource amount (non-negative; can be a fractional amount) + this.amount = this.GetMaxAmount(); +}; + +ResourceSupply.prototype.GetMaxAmount = function() +{ + return +this.template.Amount; +}; + +ResourceSupply.prototype.GetCurrentAmount = function() +{ + return this.amount; +}; + +ResourceSupply.prototype.TakeResources = function(rate) +{ + // Internally we handle fractional resource amounts (to be accurate + // over long periods of time), but want to return integers (so players + // have a nice simple integer amount of resources). So return the + // difference between rounded values: + + var old = this.amount; + this.amount = Math.max(0, old - rate/1000); + var change = Math.ceil(old) - Math.ceil(this.amount); + // (use ceil instead of floor so that we continue returning non-zero values even if + // 0 < amount < 1) + return { "amount": change, "exhausted": (old == 0) }; +}; + +ResourceSupply.prototype.GetType = function() +{ + if (this.template.Subtype) + return { "generic": this.template.Type, "specific": this.template.Subtype }; + else + return { "generic": this.template.Type }; +}; + +Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 7321) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 7322) @@ -1,141 +1,158 @@ function GuiInterface() {} GuiInterface.prototype.Init = function() { // TODO: need to not serialise this value this.placementEntity = undefined; // = undefined or [templateName, entityID] }; GuiInterface.prototype.GetSimulationState = function(player) { var ret = { "players": [] }; var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); var player = { "popCount": cmpPlayer.GetPopulationCount(), - "popLimit": cmpPlayer.GetPopulationLimit() + "popLimit": cmpPlayer.GetPopulationLimit(), + "resourceCounts": cmpPlayer.GetResourceCounts() }; ret.players.push(player); } return ret; }; GuiInterface.prototype.GetEntityState = function(player, ent) { var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var cmpPosition = Engine.QueryInterface(ent, IID_Position); var ret = { "template": cmpTempMan.GetCurrentTemplateName(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(); } + var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); + if (cmpResourceSupply) + { + ret.resourceSupply = { + "max": cmpResourceSupply.GetMaxAmount(), + "amount": cmpResourceSupply.GetCurrentAmount(), + "type": cmpResourceSupply.GetType() + }; + } + + var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); + if (cmpResourceGatherer) + { + ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); + } + return ret; }; GuiInterface.prototype.GetTemplateData = function(player, name) { var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.GetTemplate(name); var ret = {}; if (template.Identity) { ret.name = { "specific": template.Identity.SpecificName, "generic": template.Identity.GenericName, "icon_cell": template.Identity.IconCell }; } return ret; }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { var cmpSelectable = Engine.QueryInterface(cmd.entity, IID_Selectable); cmpSelectable.SetSelectionHighlight(cmd.colour); }; GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { if (!this.placementEntity || this.placementEntity[0] != cmd.template) { if (cmd.template == "") { if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); this.placementEntity = undefined; } else { this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } } if (this.placementEntity) { var pos = Engine.QueryInterface(this.placementEntity[1], IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } } }; // List the GuiInterface functions that can be safely called by GUI scripts. // (GUI scripts are non-deterministic and untrusted, so these functions must be // appropriately careful. They are called with a first argument "player", which is // trusted and indicates the player associated with the current client; no data should // be returned unless this player is meant to be able to see it.) var exposedFunctions = { "GetSimulationState": 1, "GetEntityState": 1, "GetTemplateData": 1, "SetSelectionHighlight": 1, "SetBuildingPlacementPreview": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); else throw new Error("Invalid GuiInterface Call name \""+name+"\""); }; Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 7321) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 7322) @@ -1,76 +1,52 @@ 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) +Attack.prototype.GetRange = function() { - return x*x + y*y; -} - -Attack.prototype.CheckRange = function(target) -{ - // Target must be in the world - var cmpPositionTarget = Engine.QueryInterface(target, IID_Position); - if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld()) - return { "error": "not-in-world" }; - - // We must be in the world - var cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld()) - return { "error": "not-in-world" }; - - // Target must be within range - var posTarget = cmpPositionTarget.GetPosition(); - var posSelf = cmpPositionSelf.GetPosition(); - var dist2 = hypot2(posTarget.x - posSelf.x, posTarget.z - posSelf.z); - // TODO: ought to be distance to closest point in footprint, not to center - var maxrange = +this.template.Range; - if (dist2 > maxrange*maxrange) - return { "error": "out-of-range", "maxrange": maxrange }; - - return {}; + return { "max": +this.template.Range, "min": 0 }; } /** - * Attack the target entity. This should only be called after a successful CheckRange, + * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ 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); Index: ps/trunk/binaries/data/mods/public/simulation/components/Cost.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Cost.js (revision 7321) +++ ps/trunk/binaries/data/mods/public/simulation/components/Cost.js (revision 7322) @@ -1,21 +1,31 @@ function Cost() {} Cost.prototype.Init = function() { }; Cost.prototype.GetPopCost = function() { - if ('Population' in this.template) + if ("Population" in this.template) return +this.template.Population; return 0; }; Cost.prototype.GetPopBonus = function() { - if ('PopulationBonus' in this.template) + if ("PopulationBonus" in this.template) return +this.template.PopulationBonus; return 0; }; +Cost.prototype.GetResourceCosts = function() +{ + return { + "food": +(this.template.Resources.food || 0), + "wood": +(this.template.Resources.wood || 0), + "stone": +(this.template.Resources.stone || 0), + "metal": +(this.template.Resources.metal || 0) + }; +}; + Engine.RegisterComponentType(IID_Cost, "Cost", Cost); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js (revision 7322) @@ -0,0 +1 @@ +Engine.RegisterInterface("ResourceSupply"); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js (revision 7322) @@ -0,0 +1 @@ +Engine.RegisterInterface("ResourceGatherer"); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 7322) @@ -0,0 +1,49 @@ +function ResourceGatherer() {} + +ResourceGatherer.prototype.Init = function() +{ +}; + +ResourceGatherer.prototype.GetGatherRates = function() +{ + var ret = {}; + for (var r in this.template.Rates) + ret[r] = this.template.Rates[r] * this.template.BaseSpeed; + return ret; +}; + +ResourceGatherer.prototype.GetRange = function() +{ + return { "max": 4, "min": 0 }; + // maybe this should depend on the unit or target or something? +} + +/** + * Gather from the target entity. This should only be called after a successful range check, + * and if the target has a compatible ResourceSupply. + * It should be called at a rate of once per second. + */ +ResourceGatherer.prototype.PerformGather = function(target) +{ + var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); + var type = cmpResourceSupply.GetType(); + + var rate; + if (type.specific && this.template.Rates[type.generic+"."+type.specific]) + rate = this.template.Rates[type.generic+"."+type.specific] * this.template.BaseSpeed; + else + rate = this.template.Rates[type.generic] * this.template.BaseSpeed; + + var status = cmpResourceSupply.TakeResources(rate); + + // Give the gathered resources to the player + var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(cmpOwnership.GetOwner()), IID_Player); + cmpPlayer.AddResources(type.generic, status.amount); + + return status; +}; + + +Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 7321) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 7322) @@ -1,58 +1,62 @@ Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Health.js"); +Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); +Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("GuiInterface.js"); var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface"); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { GetNumPlayers: function() { return 2; }, GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; } }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { GetCurrentTemplateName: function(ent) { return "example"; }, GetTemplate: function(name) { return ""; }, }); AddMock(100, IID_Player, { GetPopulationCount: function() { return 10; }, - GetPopulationLimit: function() { return 20; } + GetPopulationLimit: function() { return 20; }, + GetResourceCounts: function() { return { "food": 100 }; } }); AddMock(101, IID_Player, { GetPopulationCount: function() { return 40; }, - GetPopulationLimit: function() { return 30; } + GetPopulationLimit: function() { return 30; }, + GetResourceCounts: function() { return { "food": 200 }; } }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { - players: [{popCount:10, popLimit:20}, {popCount:40, popLimit:30}] + players: [{popCount:10, popLimit:20, resourceCounts:{food:100}}, {popCount:40, popLimit:30, resourceCounts:{food:200}}] }); AddMock(10, IID_Position, { GetPosition: function() { return {x:1, y:2, z:3}; } }); AddMock(10, IID_Health, { GetHitpoints: function() { return 50; } }); AddMock(10, IID_Builder, { GetEntitiesList: function() { return ["test1", "test2"]; } }); 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"] }); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 7321) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 7322) @@ -1,208 +1,355 @@ /* This is currently just a very simplistic state machine that lets units be commanded around -and then autonomously carry out the orders. +and then autonomously carry out the orders. It might need to be entirely redesigned. */ const STATE_IDLE = 0; const STATE_WALKING = 1; const STATE_ATTACKING = 2; +const STATE_GATHERING = 3; /* Attack process: * When starting attack: * 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) */ +/* Gather process is about the same, except with less synchronisation - the action + * is just performed 1sec after initiated, and then repeated every 1sec. + * (TODO: it'd be nice to avoid most of the duplication between Attack and Gather code) + */ + function UnitAI() {} UnitAI.prototype.Init = function() { 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; // Current target entity ID this.attackTarget = undefined; + + // Timer for GatherTimeout + this.gatherTimer = undefined; + // Current target entity ID + this.gatherTarget = undefined; }; //// Interface functions //// UnitAI.prototype.Walk = function(x, z) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpMotion) return; this.SelectAnimation("walk", false, cmpMotion.GetSpeed()); cmpMotion.MoveToPoint(x, z, 0, 0); this.state = STATE_WALKING; }; UnitAI.prototype.Attack = function(target) { + // Verify that we're able to respond to Attack commands var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; + // TODO: verify that this is a valid target + + // Stop any previous action timers + this.CancelTimers(); + // Remember the target, and start moving towards it this.attackTarget = target; - this.MoveToTarget(this.attackTarget); + this.MoveToTarget(target, cmpAttack.GetRange()); this.state = STATE_ATTACKING; +}; - // Cancel any previous attack timer - if (this.attackTimer) - { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.attackTimer); - this.attackTimer = undefined; - } +UnitAI.prototype.Gather = function(target) +{ + // Verify that we're able to respond to Gather commands + var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (!cmpResourceGatherer) + return; + + // TODO: verify that this is a valid target + + // Stop any previous action timers + this.CancelTimers(); + + // Remember the target, and start moving towards it + this.gatherTarget = target; + this.MoveToTarget(target, cmpResourceGatherer.GetRange()); + this.state = STATE_GATHERING; }; //// Message handlers //// UnitAI.prototype.OnDestroy = function() { - if (this.attackTimer) - { - cmpTimer.CancelTimer(this.attackTimer); - this.attackTimer = undefined; - } + // Clean up any timers that are now obsolete + this.CancelTimers(); }; UnitAI.prototype.OnMotionChanged = function(msg) { if (msg.speed) { // Started moving // => play the appropriate animation this.SelectAnimation("walk", false, msg.speed); } else { if (this.state == STATE_WALKING) { // Stopped walking this.state = STATE_IDLE; this.SelectAnimation("idle"); } else if (this.state == STATE_ATTACKING) { // We were attacking, and have stopped moving // => check if we can still reach the target now if (!this.MoveIntoAttackRange()) return; // In range, so perform the attack, // after the prepare time but not before the previous attack's recharge var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var timers = cmpAttack.GetTimers(); var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime()); this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, {}); // Start the idle animation before we switch to the attack this.SelectAnimation("idle"); } + else if (this.state == STATE_GATHERING) + { + // We were gathering, and have stopped moving + // => check if we can still reach the target now + + if (!this.MoveIntoGatherRange()) + return; + + // In range, so perform the gathering + + var cmpResourceSupply = Engine.QueryInterface(this.gatherTarget, IID_ResourceSupply); + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + + this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {}); + + // Start the gather animation + var type = cmpResourceSupply.GetType(); + var anim = "gather_" + (type.specific || type.generic); + this.SelectAnimation(anim); + } } }; //// Private functions //// +function hypot2(x, y) +{ + return x*x + y*y; +} + +UnitAI.prototype.CheckRange = function(target, range) +{ + // Target must be in the world + var cmpPositionTarget = Engine.QueryInterface(target, IID_Position); + if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld()) + return { "error": "not-in-world" }; + + // We must be in the world + var cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld()) + return { "error": "not-in-world" }; + + // Target must be within range + var posTarget = cmpPositionTarget.GetPosition(); + var posSelf = cmpPositionSelf.GetPosition(); + var dist2 = hypot2(posTarget.x - posSelf.x, posTarget.z - posSelf.z); + // TODO: ought to be distance to closest point in footprint, not to center + // The +4 is a hack to give a ~1 tile tolerance, because the pathfinder doesn't + // always get quite close enough to the target + if (dist2 > (range.max+4)*(range.max+4)) + return { "error": "out-of-range" }; + + return {}; +} + +UnitAI.prototype.CancelTimers = function() +{ + if (this.attackTimer) + { + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.attackTimer); + this.attackTimer = undefined; + } + + if (this.gatherTimer) + { + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.gatherTimer); + this.gatherTimer = undefined; + } +}; + /** * Tries to move into range of the attack target. * Returns true if it's already in range. */ UnitAI.prototype.MoveIntoAttackRange = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + var range = cmpAttack.GetRange(); - var rangeStatus = cmpAttack.CheckRange(this.attackTarget); + var rangeStatus = this.CheckRange(this.attackTarget, range); if (rangeStatus.error) { if (rangeStatus.error == "out-of-range") { // Out of range => need to move closer // (The target has probably moved while we were chasing it) - this.MoveToTarget(this.attackTarget); + this.MoveToTarget(this.attackTarget, range); + return false; + } + + // Otherwise it's impossible to reach the target, so give up + // and switch back to idle + this.state = STATE_IDLE; + this.SelectAnimation("idle"); + return false; + } + + return true; +}; + +UnitAI.prototype.MoveIntoGatherRange = function() +{ + var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + var range = cmpResourceGatherer.GetRange(); + + var rangeStatus = this.CheckRange(this.gatherTarget, range); + if (rangeStatus.error) + { + if (rangeStatus.error == "out-of-range") + { + // Out of range => need to move closer + // (The target has probably moved while we were chasing it) + this.MoveToTarget(this.gatherTarget, range); return false; } // Otherwise it's impossible to reach the target, so give up // and switch back to idle this.state = STATE_IDLE; this.SelectAnimation("idle"); return false; } return true; }; UnitAI.prototype.SelectAnimation = function(name, once, speed) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation(name, once, speed); }; -UnitAI.prototype.MoveToTarget = function(target) +UnitAI.prototype.MoveToTarget = function(target, range) { var cmpPositionTarget = Engine.QueryInterface(target, IID_Position); if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld()) return; var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); var pos = cmpPositionTarget.GetPosition(); - cmpMotion.MoveToPoint(pos.x, pos.z, 0, 1); + cmpMotion.MoveToPoint(pos.x, pos.z, range.min, range.max); }; UnitAI.prototype.AttackTimeout = function(data) { // If we stopped attacking before this timeout, then don't do any processing here if (this.state != STATE_ATTACKING) return; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); // Check if we can still reach the target if (!this.MoveIntoAttackRange()) return; // Play the attack animation this.SelectAnimation("melee", false, 1); // Hit the target cmpAttack.PerformAttack(this.attackTarget); // Set a timer to hit the target again var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var timers = cmpAttack.GetTimers(); this.attackRechargeTime = cmpTimer.GetTime() + timers.recharge; this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", timers.repeat, data); }; +UnitAI.prototype.GatherTimeout = function(data) +{ + // If we stopped gathering before this timeout, then don't do any processing here + if (this.state != STATE_GATHERING) + return; + + var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + + // Check if we can still reach the target + if (!this.MoveIntoGatherRange()) + return; + + // Gather from the target + var status = cmpResourceGatherer.PerformGather(this.gatherTarget); + + // If the resource is exhausted, then stop and go back to idle + if (status.exhausted) + { + this.state = STATE_IDLE; + this.SelectAnimation("idle"); + return; + } + + // Set a timer to gather again + + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, data); +}; + Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 7321) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 7322) @@ -1,50 +1,66 @@ function Player() {} Player.prototype.Init = function() { this.playerID = undefined; this.playerName = "Unknown"; this.civ = "celt"; this.popCount = 0; this.popLimit = 50; + this.resourceCount = { + "food": 100, + "wood": 50, + "metal": 0, + "stone": 0 + }; }; Player.prototype.SetPlayerID = function(id) { this.playerID = id; }; Player.prototype.GetPopulationCount = function() { return this.popCount; }; Player.prototype.GetPopulationLimit = function() { return this.popLimit; }; +Player.prototype.GetResourceCounts = function() +{ + return this.resourceCount; +}; + +Player.prototype.AddResources = function(type, amount) +{ + this.resourceCount[type] += (+amount); +}; + Player.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from == this.playerID) { var cost = Engine.QueryInterface(msg.entity, IID_Cost); if (cost) { this.popCount -= cost.GetPopCost(); this.popLimit += cost.GetPopBonus(); } } if (msg.to == this.playerID) { var cost = Engine.QueryInterface(msg.entity, IID_Cost); if (cost) { this.popCount += cost.GetPopCost(); this.popLimit -= cost.GetPopBonus(); } } }; Engine.RegisterComponentType(IID_Player, "Player", Player);