diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index 2adf2e443b..9e51cae7df 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -30,7 +30,9 @@ Attack.prototype.GetAttackStrengths = function() Attack.prototype.GetRange = function() { - return { "max": +this.template.Range, "min": 0 }; + var max = +this.template.Range; + var min = +(this.template.MinRange || 0); + return { "max": max, "min": min }; } /** @@ -39,10 +41,94 @@ Attack.prototype.GetRange = function() * call to PerformAttack. */ Attack.prototype.PerformAttack = function(target) +{ + // If this is a ranged attack, then launch a projectile + if (this.template.ProjectileSpeed) + { + // To implement (in)accuracy, for arrows and javelins, we want to do the following: + // * Compute an accuracy rating, based on the entity's characteristics and the distance to the target + // * Pick a random point 'close' to the target (based on the accuracy) which is the real target point + // * Pick a real target unit, based on their footprint's proximity to the real target point + // * If there is none, then harmlessly shoot to the real target point instead + // * If the real target unit moves after being targeted, the projectile will follow it and hit it anyway + // + // In the future this should be extended: + // * If the target unit moves too far, the projectile should 'detach' and not hit it, so that + // players can dodge projectiles. (Or it should pick a new target after detaching, so it can still + // hit somebody.) + // * Obstacles like trees could reduce the probability of the target being hit + // * Obstacles like walls should block projectiles entirely + // * There should be more control over the probabilities of hitting enemy units vs friendly units vs missing, + // for gameplay balance tweaks + // * Larger, slower projectiles (catapults etc) shouldn't pick targets first, they should just + // hurt anybody near their landing point + + // Get some data about the entity + var horizSpeed = +this.template.ProjectileSpeed; + var gravity = 9.81; // this affects the shape of the curve; assume it's constant for now + var accuracy = 6; // TODO: get from entity template + + //horizSpeed /= 8; gravity /= 8; // slow it down for testing + + // Find the distance to the target + var selfPosition = Engine.QueryInterface(this.entity, IID_Position).GetPosition(); + var targetPosition = Engine.QueryInterface(target, IID_Position).GetPosition(); + var horizDistance = Math.sqrt(Math.pow(targetPosition.x - selfPosition.x, 2) + Math.pow(targetPosition.z - selfPosition.z, 2)); + + // Compute the real target point (based on accuracy) + var angle = Math.random() * 2*Math.PI; + var r = 1 - Math.sqrt(Math.random()); // triangular distribution [0,1] (cluster around the center) + var offset = r * accuracy; // TODO: should be affected by range + var offsetX = offset * Math.sin(angle); + var offsetZ = offset * Math.cos(angle); + + var realTargetPosition = { "x": targetPosition.x + offsetX, "y": targetPosition.y, "z": targetPosition.z + offsetZ }; + + // TODO: what we should really do here is select the unit whose footprint is closest to the realTargetPosition + // (and harmlessly hit the ground if there's none), but as a simplification let's just randomly decide whether to + // hit the original target or not. + var realTargetUnit = undefined; + if (Math.random() < 0.5) // TODO: this is yucky and hardcoded + { + // Hit the original target + realTargetUnit = target; + realTargetPosition = targetPosition; + } + else + { + // Hit the ground + // TODO: ought to make sure Y is on the ground + } + + // Hurt the target after the appropriate time + if (realTargetUnit) + { + var realHorizDistance = Math.sqrt(Math.pow(realTargetPosition.x - selfPosition.x, 2) + Math.pow(realTargetPosition.z - selfPosition.z, 2)); + var timeToTarget = realHorizDistance / horizSpeed; + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.SetTimeout(this.entity, IID_Attack, "CauseDamage", timeToTarget*1000, target); + } + + // Launch the graphical projectile + var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); + if (realTargetUnit) + cmpProjectileManager.LaunchProjectileAtEntity(this.entity, realTargetUnit, horizSpeed, gravity); + else + cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); + } + else + { + // Melee attack - hurt the target immediately + this.CauseDamage(target); + } +}; + + +// Inflict damage on the target +Attack.prototype.CauseDamage = function(target) { var strengths = this.GetAttackStrengths(); - // Inflict damage on the target var cmpDamageReceiver = Engine.QueryInterface(target, IID_DamageReceiver); if (!cmpDamageReceiver) return; diff --git a/source/graphics/UnitManager.h b/source/graphics/UnitManager.h index 9cd4e63d65..4a66c1cf98 100644 --- a/source/graphics/UnitManager.h +++ b/source/graphics/UnitManager.h @@ -26,10 +26,8 @@ #include class CUnit; -class CModel; class CVector3D; class CEntity; -class CStr; class CObjectManager; /////////////////////////////////////////////////////////////////////////////// diff --git a/source/maths/Vector3D.cpp b/source/maths/Vector3D.cpp index ca49e9a90f..48f26446df 100644 --- a/source/maths/Vector3D.cpp +++ b/source/maths/Vector3D.cpp @@ -27,6 +27,12 @@ #include #include #include "MathUtil.h" +#include "FixedVector3D.h" + +CVector3D::CVector3D(const CFixedVector3D& v) : + X(v.X.ToFloat()), Y(v.Y.ToFloat()), Z(v.Z.ToFloat()) +{ +} int CVector3D::operator ! () const { diff --git a/source/maths/Vector3D.h b/source/maths/Vector3D.h index a4949a2b98..73abd8c811 100644 --- a/source/maths/Vector3D.h +++ b/source/maths/Vector3D.h @@ -23,6 +23,8 @@ #ifndef INCLUDED_VECTOR3D #define INCLUDED_VECTOR3D +class CFixedVector3D; + class CVector3D { public: @@ -31,6 +33,7 @@ class CVector3D public: CVector3D () : X(0.0f), Y(0.0f), Z(0.0f) {} CVector3D (float x, float y, float z) : X(x), Y(y), Z(z) {} + CVector3D (const CFixedVector3D& v); int operator!() const; diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index 19ad7b276e..f9853f3cc1 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -58,9 +58,11 @@ public: // Add native system components: m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_TemplateManager, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Terrain, noParam); + m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_CommandQueue, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Pathfinder, noParam); + m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_ProjectileManager, noParam); + m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Terrain, noParam); // Add scripted system components: if (!skipScriptedComponents) diff --git a/source/simulation2/TypeList.h b/source/simulation2/TypeList.h index 0ec3842262..2c3f19f7cc 100644 --- a/source/simulation2/TypeList.h +++ b/source/simulation2/TypeList.h @@ -81,6 +81,9 @@ COMPONENT(PlayerManagerScripted) INTERFACE(Position) COMPONENT(Position) // must be before VisualActor +INTERFACE(ProjectileManager) +COMPONENT(ProjectileManager) + INTERFACE(Selectable) COMPONENT(Selectable) diff --git a/source/simulation2/components/CCmpProjectileManager.cpp b/source/simulation2/components/CCmpProjectileManager.cpp new file mode 100644 index 0000000000..3f54005a64 --- /dev/null +++ b/source/simulation2/components/CCmpProjectileManager.cpp @@ -0,0 +1,281 @@ +/* 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 "ICmpProjectileManager.h" + +#include "ICmpPosition.h" +#include "simulation2/MessageTypes.h" + +#include "graphics/Frustum.h" +#include "graphics/Model.h" +#include "graphics/Unit.h" +#include "graphics/UnitManager.h" +#include "maths/Matrix3D.h" +#include "maths/Quaternion.h" +#include "maths/Vector3D.h" +#include "renderer/Scene.h" + +class CCmpProjectileManager : public ICmpProjectileManager +{ +public: + static void ClassInit(CComponentManager& componentManager) + { + componentManager.SubscribeToMessageType(MT_Interpolate); + componentManager.SubscribeToMessageType(MT_RenderSubmit); + } + + DEFAULT_COMPONENT_ALLOCATOR(ProjectileManager) + + virtual void Init(const CSimContext& context, const CParamNode& UNUSED(paramNode)) + { + m_Context = &context; + } + + virtual void Deinit(const CSimContext& UNUSED(context)) + { + } + + virtual void Serialize(ISerializer& UNUSED(serialize)) + { + // Because this is just graphical effects, and because it's all non-deterministic floating point, + // we don't do any serialization here. + // (That means projectiles will vanish if you save/load - is that okay?) + } + + virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) + { + Init(context, paramNode); + } + + virtual void HandleMessage(const CSimContext& context, const CMessage& msg, bool UNUSED(global)) + { + switch (msg.GetType()) + { + case MT_Interpolate: + { + const CMessageInterpolate& msgData = static_cast (msg); + Interpolate(context, msgData.frameTime, msgData.offset); + break; + } + case MT_RenderSubmit: + { + const CMessageRenderSubmit& msgData = static_cast (msg); + RenderSubmit(context, msgData.collector, msgData.frustum, msgData.culling); + break; + } + } + } + + virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, CFixed_23_8 speed, CFixed_23_8 gravity) + { + LaunchProjectile(source, CFixedVector3D(), target, speed, gravity); + } + + virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, CFixed_23_8 speed, CFixed_23_8 gravity) + { + LaunchProjectile(source, target, INVALID_ENTITY, speed, gravity); + } + +private: + const CSimContext* m_Context; + + struct Projectile + { + CUnit* unit; + CVector3D pos; + CVector3D target; + entity_id_t targetEnt; // INVALID_ENTITY if the target is just a point + float timeLeft; + float horizSpeed; + float gravity; + }; + + std::vector m_Projectiles; + + void LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, CFixed_23_8 speed, CFixed_23_8 gravity); + + void AdvanceProjectile(const CSimContext& context, Projectile& projectile, float dt, float frameOffset); + + void Interpolate(const CSimContext& context, float frameTime, float frameOffset); + + void RenderSubmit(const CSimContext& context, SceneCollector& collector, const CFrustum& frustum, bool culling); +}; + +REGISTER_COMPONENT_TYPE(ProjectileManager) + +void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, CFixed_23_8 speed, CFixed_23_8 gravity) +{ + if (!m_Context->HasUnitManager()) + return; // do nothing if graphics are disabled + + Projectile projectile; + + std::set selections; + std::string name = "props/units/weapons/arrow_front.xml"; // TODO: get from somewhere proper (entity or actor?) + projectile.unit = m_Context->GetUnitManager().CreateUnit(name, NULL, selections); + if (!projectile.unit) + { + // The error will have already been logged + return; + } + + CmpPtr sourcePos(*m_Context, source); + if (sourcePos.null()) + return; + + CVector3D sourceVec(sourcePos->GetPosition()); + sourceVec.Y += 3.f; // TODO: ought to exactly match the appropriate prop point + + CVector3D targetVec; + + if (targetEnt == INVALID_ENTITY) + { + targetVec = CVector3D(targetPoint); + } + else + { + CmpPtr targetPos(*m_Context, targetEnt); + if (targetPos.null()) + return; + + targetVec = CVector3D(targetPos->GetPosition()); + } + + projectile.pos = sourceVec; + projectile.target = targetVec; + projectile.targetEnt = targetEnt; + + CVector3D offset = projectile.target - projectile.pos; + float horizDistance = hypot(offset.X, offset.Z); + + projectile.horizSpeed = speed.ToFloat(); + projectile.timeLeft = horizDistance / projectile.horizSpeed; + + projectile.gravity = gravity.ToFloat(); + + m_Projectiles.push_back(projectile); +} + +void CCmpProjectileManager::AdvanceProjectile(const CSimContext& context, Projectile& projectile, float dt, float frameOffset) +{ + // Do nothing if we've already reached the target + if (projectile.timeLeft <= 0) + return; + + // Clamp dt to the remaining travel time + if (dt > projectile.timeLeft) + dt = projectile.timeLeft; + + // Track the target entity (if there is one, and it's still alive) + if (projectile.targetEnt != INVALID_ENTITY) + { + CmpPtr targetPos(context, projectile.targetEnt); + if (!targetPos.null()) + { + CMatrix3D t = targetPos->GetInterpolatedTransform(frameOffset); + projectile.target = t.GetTranslation(); + projectile.target.Y += 2.f; // TODO: ought to aim towards a random point in the solid body of the target + + // TODO: if the unit is moving, we should probably aim a bit in front of it + // so we don't have to curve so much just before reaching it + } + } + + CVector3D offset = projectile.target - projectile.pos; + + // Compute the vertical velocity that's needed so we travel in a ballistic curve and + // reach the target after timeLeft. + // (This is just a linear approximation to the curve, but it'll converge to hit the target) + float vh = (projectile.gravity / 2.f) * projectile.timeLeft + offset.Y / projectile.timeLeft; + + // Move an appropriate fraction towards the target + CVector3D delta (offset.X * dt/projectile.timeLeft, vh * dt, offset.Z * dt/projectile.timeLeft); + + projectile.pos += delta; + projectile.timeLeft -= dt; + + // If we're really close to the target now, delete the remaining time so that we don't do a little + // tiny numerically-imprecise movement next frame + if (projectile.timeLeft < 0.01f) + projectile.timeLeft = 0; + + // Construct a rotation matrix so that (0,1,0) is in the direction of 'delta' + + CMatrix3D transform; + CVector3D up(0, 1, 0); + + delta.Normalize(); + CVector3D axis = up.Cross(delta); + if (axis.LengthSquared() < 0.0001f) + axis = CVector3D(1, 0, 0); // if up & delta are almost collinear, rotate around some other arbitrary axis + else + axis.Normalize(); + + float angle = acos(up.Dot(delta)); + CQuaternion quat; + quat.FromAxisAngle(axis, angle); + quat.ToMatrix(transform); + + // Then apply the translation + transform.Translate(projectile.pos); + + // Move the model + projectile.unit->GetModel()->SetTransform(transform); +} + +void CCmpProjectileManager::Interpolate(const CSimContext& context, float frameTime, float frameOffset) +{ + for (size_t i = 0; i < m_Projectiles.size(); ++i) + { + AdvanceProjectile(context, m_Projectiles[i], frameTime, frameOffset); + } + + // Remove the ones that have reached their target + for (size_t i = 0; i < m_Projectiles.size(); ) + { + // Only remove ones that were hitting entities - leave the ones that hit the ground, because + // it looks pretty. (TODO: they should expire after some limit, not stay forever) + if (m_Projectiles[i].timeLeft <= 0 && m_Projectiles[i].targetEnt != INVALID_ENTITY) + { + // Delete in-place by swapping with the last in the list + std::swap(m_Projectiles[i], m_Projectiles.back()); + m_Projectiles.pop_back(); + } + else + ++i; + } +} + +void CCmpProjectileManager::RenderSubmit(const CSimContext& UNUSED(context), SceneCollector& collector, const CFrustum& frustum, bool culling) +{ + for (size_t i = 0; i < m_Projectiles.size(); ++i) + { + CModel* model = m_Projectiles[i].unit->GetModel(); + + model->ValidatePosition(); + + if (culling && !frustum.IsBoxVisible(CVector3D(0, 0, 0), model->GetBounds())) + continue; + + // TODO: do something about LOS (copy from CCmpVisualActor) + + collector.SubmitRecursive(model); + } +} diff --git a/source/simulation2/components/ICmpProjectileManager.cpp b/source/simulation2/components/ICmpProjectileManager.cpp new file mode 100644 index 0000000000..8365cb0a43 --- /dev/null +++ b/source/simulation2/components/ICmpProjectileManager.cpp @@ -0,0 +1,27 @@ +/* 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 "ICmpProjectileManager.h" + +#include "simulation2/system/InterfaceScripted.h" + +BEGIN_INTERFACE_WRAPPER(ProjectileManager) +DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtEntity", void, ICmpProjectileManager, LaunchProjectileAtEntity, entity_id_t, entity_id_t, CFixed_23_8, CFixed_23_8) +DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", void, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, CFixed_23_8, CFixed_23_8) +END_INTERFACE_WRAPPER(ProjectileManager) diff --git a/source/simulation2/components/ICmpProjectileManager.h b/source/simulation2/components/ICmpProjectileManager.h new file mode 100644 index 0000000000..434226f0d8 --- /dev/null +++ b/source/simulation2/components/ICmpProjectileManager.h @@ -0,0 +1,55 @@ +/* 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_ICMPPROJECTILEMANAGER +#define INCLUDED_ICMPPROJECTILEMANAGER + +#include "simulation2/system/Interface.h" + +#include "maths/Fixed.h" +#include "maths/FixedVector3D.h" + +/** + * Projectile manager. This deals with the rendering and the graphical motion of projectiles. + * (The gameplay effects of projectiles are not handled here - the simulation code does that + * with timers, this just does the visual aspects, because it's simpler to keep the parts separated.) + */ +class ICmpProjectileManager : public IComponent +{ +public: + /** + * Launch a projectile from entity @p source to entity @p target. + * @param source source entity; the projectile will determined from the "projectile" prop in its actor + * @param target target entity; the projectile will automatically track the target to ensure it always hits precisely + * @param speed horizontal speed in m/s + * @param gravity gravitational acceleration in m/s^2 (determines the height of the ballistic curve) + */ + virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, CFixed_23_8 speed, CFixed_23_8 gravity) = 0; + + /** + * Launch a projectile from entity @p source to point @p target. + * @param source source entity; the projectile will determined from the "projectile" prop in its actor + * @param target target point + * @param speed horizontal speed in m/s + * @param gravity gravitational acceleration in m/s^2 (determines the height of the ballistic curve) + */ + virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, CFixed_23_8 speed, CFixed_23_8 gravity) = 0; + + DECLARE_INTERFACE_TYPE(ProjectileManager) +}; + +#endif // INCLUDED_ICMPPROJECTILEMANAGER diff --git a/source/simulation2/scripting/EngineScriptConversions.cpp b/source/simulation2/scripting/EngineScriptConversions.cpp index 3d15efec95..284bc30fb6 100644 --- a/source/simulation2/scripting/EngineScriptConversions.cpp +++ b/source/simulation2/scripting/EngineScriptConversions.cpp @@ -189,6 +189,30 @@ template<> jsval ScriptInterface::ToJSVal(JSContext* cx, const CFix return rval; } +template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval v, CFixedVector3D& out) +{ + ScriptInterface::LocalRootScope scope(cx); + if (!scope.OK()) + return false; + + if (!JSVAL_IS_OBJECT(v)) + return false; // TODO: report type error + JSObject* obj = JSVAL_TO_OBJECT(v); + + jsval p; + + if (!JS_GetProperty(cx, obj, "x", &p)) return false; // TODO: report type errors + if (!FromJSVal(cx, p, out.X)) return false; + + if (!JS_GetProperty(cx, obj, "y", &p)) return false; + if (!FromJSVal(cx, p, out.Y)) return false; + + if (!JS_GetProperty(cx, obj, "z", &p)) return false; + if (!FromJSVal(cx, p, out.Z)) return false; + + return true; +} + template<> jsval ScriptInterface::ToJSVal(JSContext* cx, const CFixedVector3D& val) { ScriptInterface::LocalRootScope scope(cx);