Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml (revision 11886)
@@ -1,68 +1,69 @@
2.02.02.00.040.00.070.010.060.020002000
+ 1.52122010010010.0200Support Infantry Cavalry110800Light WarshipLight Warship.Warshipattack/impact/arrow_metal.xml6.00.56.012.518.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 11886)
@@ -1,92 +1,93 @@
20.040.020.00.025.00.080.012.075.012002000
+ 1.531Fortress1042006508.0200.075Support Infantry Cavalry Siege024200FortressTrain heroes, champions, and siege weapons. Garrison: 20.City Defensive GarrisonFortressstructures/fortress.pngphase_city10000650
pair_champ_02
siege_attack
siege_armor
interface/complete/building/complete_fortress.xmlattack/weapon/arrowfly.xmlattack/destruction/building_collapse_large.xmlfalse1006553680structures/fndn_6x6.xml
Index: ps/trunk/source/simulation2/components/CCmpProjectileManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpProjectileManager.cpp (revision 11885)
+++ ps/trunk/source/simulation2/components/CCmpProjectileManager.cpp (revision 11886)
@@ -1,357 +1,348 @@
/* Copyright (C) 2012 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 "ICmpObstruction.h"
+#include "ICmpObstructionManager.h"
#include "ICmpPosition.h"
#include "ICmpRangeManager.h"
#include "ICmpTerrain.h"
#include "ICmpVisual.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 "ps/CLogger.h"
#include "renderer/Scene.h"
// Time (in seconds) before projectiles that stuck in the ground are destroyed
const static float PROJECTILE_DECAY_TIME = 30.f;
class CCmpProjectileManager : public ICmpProjectileManager
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Interpolate);
componentManager.SubscribeToMessageType(MT_RenderSubmit);
}
DEFAULT_COMPONENT_ALLOCATOR(ProjectileManager)
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_ActorSeed = 0;
+ m_Id = 1;
}
virtual void Deinit()
{
for (size_t i = 0; i < m_Projectiles.size(); ++i)
GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles[i].unit);
m_Projectiles.clear();
}
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 CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
{
Init(paramNode);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Interpolate:
{
const CMessageInterpolate& msgData = static_cast (msg);
- Interpolate(msgData.frameTime, msgData.offset);
+ Interpolate(msgData.frameTime);
break;
}
case MT_RenderSubmit:
{
const CMessageRenderSubmit& msgData = static_cast (msg);
RenderSubmit(msgData.collector, msgData.frustum, msgData.culling);
break;
}
}
}
- virtual void LaunchProjectileAtEntity(entity_id_t source, entity_id_t target, fixed speed, fixed gravity)
+ virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity)
{
- LaunchProjectile(source, CFixedVector3D(), target, speed, gravity);
- }
-
- virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity)
- {
- LaunchProjectile(source, target, INVALID_ENTITY, speed, gravity);
+ return LaunchProjectile(source, target, speed, gravity);
}
+
+ virtual void RemoveProjectile(uint32_t);
private:
struct Projectile
{
CUnit* unit;
CVector3D pos;
CVector3D target;
- entity_id_t targetEnt; // INVALID_ENTITY if the target is just a point
float timeLeft;
float speedFactor;
float gravity;
bool stopped;
+ uint32_t id;
};
std::vector m_Projectiles;
uint32_t m_ActorSeed;
+
+ uint32_t m_Id;
- void LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity);
+ uint32_t LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity);
- void AdvanceProjectile(Projectile& projectile, float dt, float frameOffset);
+ void AdvanceProjectile(Projectile& projectile, float dt);
- void Interpolate(float frameTime, float frameOffset);
+ void Interpolate(float frameTime);
void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling);
};
REGISTER_COMPONENT_TYPE(ProjectileManager)
-void CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, entity_id_t targetEnt, fixed speed, fixed gravity)
+uint32_t CCmpProjectileManager::LaunchProjectile(entity_id_t source, CFixedVector3D targetPoint, fixed speed, fixed gravity)
{
if (!GetSimContext().HasUnitManager())
- return; // do nothing if graphics are disabled
+ return 0; // do nothing if graphics are disabled
CmpPtr cmpSourceVisual(GetSimContext(), source);
if (!cmpSourceVisual)
- return;
+ return 0;
std::wstring name = cmpSourceVisual->GetProjectileActor();
if (name.empty())
{
// If the actor was actually loaded, complain that it doesn't have a projectile
if (!cmpSourceVisual->GetActorShortName().empty())
LOGERROR(L"Unit with actor '%ls' launched a projectile but has no actor on 'projectile' attachpoint", cmpSourceVisual->GetActorShortName().c_str());
- return;
+ return 0;
}
CVector3D sourceVec(cmpSourceVisual->GetProjectileLaunchPoint());
if (!sourceVec)
{
// If there's no explicit launch point, take a guess based on the entity position
CmpPtr sourcePos(GetSimContext(), source);
if (!sourcePos)
- return;
+ return 0;
sourceVec = sourcePos->GetPosition();
sourceVec.Y += 3.f;
}
CVector3D targetVec;
- if (targetEnt == INVALID_ENTITY)
- {
- targetVec = CVector3D(targetPoint);
- }
- else
- {
- CmpPtr cmpTargetPosition(GetSimContext(), targetEnt);
- if (!cmpTargetPosition)
- return;
-
- targetVec = CVector3D(cmpTargetPosition->GetPosition());
- }
+ targetVec = CVector3D(targetPoint);
Projectile projectile;
std::set selections;
projectile.unit = GetSimContext().GetUnitManager().CreateUnit(name, m_ActorSeed++, selections);
+ projectile.id = m_Id++;
if (!projectile.unit)
{
// The error will have already been logged
- return;
+ return 0;
}
projectile.pos = sourceVec;
projectile.target = targetVec;
- projectile.targetEnt = targetEnt;
CVector3D offset = projectile.target - projectile.pos;
float horizDistance = sqrtf(offset.X*offset.X + offset.Z*offset.Z);
projectile.speedFactor = 1.f;
projectile.timeLeft = horizDistance / speed.ToFloat();
projectile.stopped = false;
projectile.gravity = gravity.ToFloat();
m_Projectiles.push_back(projectile);
+
+ return projectile.id;
}
-void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt, float frameOffset)
+void CCmpProjectileManager::AdvanceProjectile(Projectile& projectile, float dt)
{
// Do special processing if we've already reached the target
if (projectile.timeLeft <= 0)
{
if (projectile.stopped)
{
projectile.timeLeft -= dt;
return;
}
// else continue moving the projectile
// To prevent arrows going crazily far after missing the target,
// apply a bit of drag to them
projectile.speedFactor *= powf(1.0f - 0.4f*projectile.speedFactor, dt);
}
- else
- {
- // Projectile hasn't reached the target yet:
- // Track the target entity (if there is one, and it's still alive)
- if (projectile.targetEnt != INVALID_ENTITY)
- {
- CmpPtr cmpTargetPosition(GetSimContext(), projectile.targetEnt);
- if (cmpTargetPosition && cmpTargetPosition->IsInWorld())
- {
- CMatrix3D t = cmpTargetPosition->GetInterpolatedTransform(frameOffset, false);
- 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) * projectile.speedFactor;
// 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've passed the target position and haven't stopped yet,
// carry on until we reach solid land
if (projectile.timeLeft <= 0)
{
CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
if (cmpTerrain)
{
float h = cmpTerrain->GetExactGroundLevel(projectile.pos.X, projectile.pos.Z);
if (projectile.pos.Y < h)
{
projectile.pos.Y = h; // stick precisely to the terrain
projectile.stopped = true;
}
}
}
// Construct a rotation matrix so that (0,1,0) is in the direction of 'delta'
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 = acosf(up.Dot(delta));
CMatrix3D transform;
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(float frameTime, float frameOffset)
+void CCmpProjectileManager::Interpolate(float frameTime)
{
for (size_t i = 0; i < m_Projectiles.size(); ++i)
{
- AdvanceProjectile(m_Projectiles[i], frameTime, frameOffset);
+ AdvanceProjectile(m_Projectiles[i], frameTime);
}
// Remove the ones that have reached their target
for (size_t i = 0; i < m_Projectiles.size(); )
{
// Projectiles hitting targets get removed immediately.
// Those hitting the ground stay for a while, because it looks pretty.
if (m_Projectiles[i].timeLeft <= 0.f)
{
- if (m_Projectiles[i].targetEnt == INVALID_ENTITY && m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
+ if (m_Projectiles[i].timeLeft > -PROJECTILE_DECAY_TIME)
{
// Keep the projectile until it exceeds the decay time
}
else
{
// Delete in-place by swapping with the last in the list
std::swap(m_Projectiles[i], m_Projectiles.back());
GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
m_Projectiles.pop_back();
continue; // don't increment i
}
}
++i;
}
}
+void CCmpProjectileManager::RemoveProjectile(uint32_t id)
+{
+ // Scan through the projectile list looking for one with the correct id to remove
+ for (size_t i = 0; i < m_Projectiles.size(); i++)
+ {
+ if (m_Projectiles[i].id == id)
+ {
+ // Delete in-place by swapping with the last in the list
+ std::swap(m_Projectiles[i], m_Projectiles.back());
+ GetSimContext().GetUnitManager().DeleteUnit(m_Projectiles.back().unit);
+ m_Projectiles.pop_back();
+ return;
+ }
+ }
+}
+
void CCmpProjectileManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling)
{
CmpPtr cmpRangeManager(GetSimContext(), SYSTEM_ENTITY);
int player = GetSimContext().GetCurrentDisplayedPlayer();
ICmpRangeManager::CLosQuerier los (cmpRangeManager->GetLosQuerier(player));
bool losRevealAll = cmpRangeManager->GetLosRevealAll(player);
for (size_t i = 0; i < m_Projectiles.size(); ++i)
{
// Don't display projectiles outside the visible area
ssize_t posi = (ssize_t)(0.5f + m_Projectiles[i].pos.X / TERRAIN_TILE_SIZE);
ssize_t posj = (ssize_t)(0.5f + m_Projectiles[i].pos.Z / TERRAIN_TILE_SIZE);
if (!losRevealAll && !los.IsVisible(posi, posj))
continue;
CModelAbstract& model = m_Projectiles[i].unit->GetModel();
model.ValidatePosition();
if (culling && !frustum.IsBoxVisible(CVector3D(0, 0, 0), model.GetWorldBoundsRec()))
continue;
// TODO: do something about LOS (copy from CCmpVisualActor)
collector.SubmitRecursive(&model);
}
}
Index: ps/trunk/source/simulation2/components/ICmpProjectileManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpProjectileManager.cpp (revision 11885)
+++ ps/trunk/source/simulation2/components/ICmpProjectileManager.cpp (revision 11886)
@@ -1,27 +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, fixed, fixed)
-DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", void, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed)
+DEFINE_INTERFACE_METHOD_4("LaunchProjectileAtPoint", uint32_t, ICmpProjectileManager, LaunchProjectileAtPoint, entity_id_t, CFixedVector3D, fixed, fixed)
+DEFINE_INTERFACE_METHOD_1("RemoveProjectile", void, ICmpProjectileManager, RemoveProjectile, uint32_t)
END_INTERFACE_WRAPPER(ProjectileManager)
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml (revision 11886)
@@ -1,26 +1,27 @@
0.010.00.016.010.025.09001500OrganicStoneWall
+ 1.5Ranged CavalryRangedattack/weapon/arrowfly.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_tower.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_tower.xml (revision 11886)
@@ -1,86 +1,87 @@
20.040.015.00.025.00.040.012.075.012002000
+ 1.511Wall201008.050.1Support Infantry024000Wall TurretShoots arrows. Garrison to defend a city wall against attackers.Defensive -ConquestCritical StoneWall Towerstructures/tower.pngphase_town100010150interface/complete/building/complete_tower.xmlattack/weapon/arrowfly.xmlattack/destruction/building_collapse_large.xml20.0false206553660structures/fndn_2x2.xml
Index: ps/trunk/source/simulation2/components/ICmpPosition.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpPosition.h (revision 11885)
+++ ps/trunk/source/simulation2/components/ICmpPosition.h (revision 11886)
@@ -1,161 +1,174 @@
/* Copyright (C) 2011 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_ICMPPOSITION
#define INCLUDED_ICMPPOSITION
#include "simulation2/system/Interface.h"
#include "simulation2/helpers/Position.h"
#include "maths/FixedVector3D.h"
#include "maths/FixedVector2D.h"
class CMatrix3D;
/**
* Represents an entity's position in the world (plus its orientation).
*
* Entity positions are determined by the following:
* - X, Z coordinates (giving left/right and front/back coordinates on the map)
* - Y offset (height; entities always snap to the ground, then are offset by this amount)
* - 'Floating' flag (snap to surface of water instead of going underneath)
* As far as the simulation code is concerned, movements are instantaneous.
* The only exception is GetInterpolatedTransform, used for rendering, which may
* interpolate between the previous and current positions.
* (The "Jump" methods circumvent the interpolation, and should be used whenever immediate
* movement is needed.)
*
* Orientations consist of the following:
* - Rotation around upwards 'Y' axis (the common form of rotation)
* - Terrain conformance mode, one of:
* - Upright (upwards axis is always the world Y axis, e.g. for humans)
* - Pitch (rotates backwards and forwards to match the terrain, e.g. for horses)
* - PitchRoll (rotates in all directions to match the terrain, e.g. for carts)
* - Rotation around relative X (pitch), Z (roll) axes (rare; used for special effects)
*
* Entities can also be 'outside the world' (e.g. hidden inside a building), in which
* case they have no position. Callers must check the entity is in the world, before
* querying its position.
*/
class ICmpPosition : public IComponent
{
public:
/**
* Returns true if the entity currently exists at a defined position in the world.
*/
virtual bool IsInWorld() = 0;
/**
* Causes IsInWorld to return false. (Use MoveTo() or JumpTo() to move back into the world.)
*/
virtual void MoveOutOfWorld() = 0;
/**
* Move smoothly to the given location.
*/
virtual void MoveTo(entity_pos_t x, entity_pos_t z) = 0;
/**
* Move immediately to the given location, with no interpolation.
*/
virtual void JumpTo(entity_pos_t x, entity_pos_t z) = 0;
/**
* Set the vertical offset above the terrain/water surface.
*/
virtual void SetHeightOffset(entity_pos_t dy) = 0;
/**
* Returns the current vertical offset above the terrain/water surface.
*/
virtual entity_pos_t GetHeightOffset() = 0;
/**
* Set the vertical position as a fixed, absolute value.
* Will stay at this height until the next call to SetHeightFixed or SetHeightOffset.
*/
virtual void SetHeightFixed(entity_pos_t y) = 0;
/**
* Returns whether the entity floats on water.
*/
virtual bool IsFloating() = 0;
/**
* Returns the current x,y,z position (no interpolation).
* Depends on the current terrain heightmap.
* Must not be called unless IsInWorld is true.
*/
virtual CFixedVector3D GetPosition() = 0;
/**
* Returns the current x,z position (no interpolation).
* Must not be called unless IsInWorld is true.
*/
virtual CFixedVector2D GetPosition2D() = 0;
+ /**
+ * Returns the previous turn's x,y,z position (no interpolation).
+ * Depends on the current terrain heightmap.
+ * Must not be called unless IsInWorld is true.
+ */
+ virtual CFixedVector3D GetPreviousPosition() = 0;
+
+ /**
+ * Returns the previous turn's x,z position (no interpolation).
+ * Must not be called unless IsInWorld is true.
+ */
+ virtual CFixedVector2D GetPreviousPosition2D() = 0;
+
/**
* Rotate smoothly to the given angle around the upwards axis.
* @param y clockwise radians from the +Z axis.
*/
virtual void TurnTo(entity_angle_t y) = 0;
/**
* Rotate immediately to the given angle around the upwards axis.
* @param y clockwise radians from the +Z axis.
*/
virtual void SetYRotation(entity_angle_t y) = 0;
/**
* Rotate immediately to the given angles around the X (pitch) and Z (roll) axes.
* @param x radians around the X axis. (TODO: in which direction?)
* @param z radians around the Z axis.
*/
virtual void SetXZRotation(entity_angle_t x, entity_angle_t z) = 0;
// NOTE: we separate Y from XZ because most code will only ever change Y;
// XZ are typically just used in the editor, and other code should never
// worry about them
/**
* Returns the current rotation (relative to the upwards axis), as Euler
* angles with X=pitch, Y=yaw, Z=roll. (TODO: is that the right way round?)
*/
virtual CFixedVector3D GetRotation() = 0;
/**
* Returns the distance that the unit will be interpolated over,
* i.e. the distance travelled since the start of the turn.
*/
virtual fixed GetDistanceTravelled() = 0;
/**
* Get the current interpolated 2D position and orientation, for rendering.
* Must not be called unless IsInWorld is true.
*/
virtual void GetInterpolatedPosition2D(float frameOffset, float& x, float& z, float& rotY) = 0;
/**
* Get the current interpolated transform matrix, for rendering.
* Must not be called unless IsInWorld is true.
*/
virtual CMatrix3D GetInterpolatedTransform(float frameOffset, bool forceFloating) = 0;
DECLARE_INTERFACE_TYPE(Position)
};
#endif // INCLUDED_ICMPPOSITION
Index: ps/trunk/source/simulation2/components/ICmpProjectileManager.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpProjectileManager.h (revision 11885)
+++ ps/trunk/source/simulation2/components/ICmpProjectileManager.h (revision 11886)
@@ -1,55 +1,54 @@
/* 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, fixed speed, fixed 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)
+ * @return id of the created projectile
*/
- virtual void LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity) = 0;
+ virtual uint32_t LaunchProjectileAtPoint(entity_id_t source, CFixedVector3D target, fixed speed, fixed gravity) = 0;
+
+ /**
+ * Removes a projectile, used when the projectile has hit a target
+ * @param id of the projectile to remove
+ */
+ virtual void RemoveProjectile(uint32_t id) = 0;
DECLARE_INTERFACE_TYPE(ProjectileManager)
};
#endif // INCLUDED_ICMPPROJECTILEMANAGER
Index: ps/trunk/source/simulation2/components/ICmpPosition.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpPosition.cpp (revision 11885)
+++ ps/trunk/source/simulation2/components/ICmpPosition.cpp (revision 11886)
@@ -1,40 +1,42 @@
/* Copyright (C) 2011 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 "ICmpPosition.h"
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(Position)
DEFINE_INTERFACE_METHOD_0("IsInWorld", bool, ICmpPosition, IsInWorld)
DEFINE_INTERFACE_METHOD_0("MoveOutOfWorld", void, ICmpPosition, MoveOutOfWorld)
DEFINE_INTERFACE_METHOD_2("MoveTo", void, ICmpPosition, MoveTo, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_2("JumpTo", void, ICmpPosition, JumpTo, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_1("SetHeightOffset", void, ICmpPosition, SetHeightOffset, entity_pos_t)
DEFINE_INTERFACE_METHOD_0("GetHeightOffset", entity_pos_t, ICmpPosition, GetHeightOffset)
DEFINE_INTERFACE_METHOD_1("SetHeightFixed", void, ICmpPosition, SetHeightFixed, entity_pos_t)
DEFINE_INTERFACE_METHOD_0("IsFloating", bool, ICmpPosition, IsFloating)
DEFINE_INTERFACE_METHOD_0("GetPosition", CFixedVector3D, ICmpPosition, GetPosition)
DEFINE_INTERFACE_METHOD_0("GetPosition2D", CFixedVector2D, ICmpPosition, GetPosition2D)
+DEFINE_INTERFACE_METHOD_0("GetPreviousPosition", CFixedVector3D, ICmpPosition, GetPreviousPosition)
+DEFINE_INTERFACE_METHOD_0("GetPreviousPosition2D", CFixedVector2D, ICmpPosition, GetPreviousPosition2D)
DEFINE_INTERFACE_METHOD_1("TurnTo", void, ICmpPosition, TurnTo, entity_angle_t)
DEFINE_INTERFACE_METHOD_1("SetYRotation", void, ICmpPosition, SetYRotation, entity_angle_t)
DEFINE_INTERFACE_METHOD_2("SetXZRotation", void, ICmpPosition, SetXZRotation, entity_angle_t, entity_angle_t)
DEFINE_INTERFACE_METHOD_0("GetRotation", CFixedVector3D, ICmpPosition, GetRotation)
// Excluded: GetInterpolatedTransform (not safe for scripts)
END_INTERFACE_WRAPPER(Position)
Index: ps/trunk/source/simulation2/components/CCmpObstructionManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpObstructionManager.cpp (revision 11885)
+++ ps/trunk/source/simulation2/components/CCmpObstructionManager.cpp (revision 11886)
@@ -1,995 +1,1004 @@
/* Copyright (C) 2012 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 "ICmpObstructionManager.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Render.h"
#include "simulation2/helpers/Spatial.h"
#include "simulation2/serialization/SerializeTemplates.h"
#include "graphics/Overlay.h"
#include "graphics/Terrain.h"
#include "maths/MathUtil.h"
#include "ps/Overlay.h"
#include "ps/Profile.h"
#include "renderer/Scene.h"
#include "ps/CLogger.h"
// Externally, tags are opaque non-zero positive integers.
// Internally, they are tagged (by shape) indexes into shape lists.
// idx must be non-zero.
#define TAG_IS_VALID(tag) ((tag).valid())
#define TAG_IS_UNIT(tag) (((tag).n & 1) == 0)
#define TAG_IS_STATIC(tag) (((tag).n & 1) == 1)
#define UNIT_INDEX_TO_TAG(idx) tag_t(((idx) << 1) | 0)
#define STATIC_INDEX_TO_TAG(idx) tag_t(((idx) << 1) | 1)
#define TAG_TO_INDEX(tag) ((tag).n >> 1)
/**
* Internal representation of axis-aligned sometimes-square sometimes-circle shapes for moving units
*/
struct UnitShape
{
entity_id_t entity;
entity_pos_t x, z;
entity_pos_t r; // radius of circle, or half width of square
ICmpObstructionManager::flags_t flags;
entity_id_t group; // control group (typically the owner entity, or a formation controller entity) (units ignore collisions with others in the same group)
};
/**
* Internal representation of arbitrary-rotation static square shapes for buildings
*/
struct StaticShape
{
entity_id_t entity;
entity_pos_t x, z; // world-space coordinates
CFixedVector2D u, v; // orthogonal unit vectors - axes of local coordinate space
entity_pos_t hw, hh; // half width/height in local coordinate space
ICmpObstructionManager::flags_t flags;
entity_id_t group;
entity_id_t group2;
};
/**
* Serialization helper template for UnitShape
*/
struct SerializeUnitShape
{
template
void operator()(S& serialize, const char* UNUSED(name), UnitShape& value)
{
serialize.NumberU32_Unbounded("entity", value.entity);
serialize.NumberFixed_Unbounded("x", value.x);
serialize.NumberFixed_Unbounded("z", value.z);
serialize.NumberFixed_Unbounded("r", value.r);
serialize.NumberU8_Unbounded("flags", value.flags);
serialize.NumberU32_Unbounded("group", value.group);
}
};
/**
* Serialization helper template for StaticShape
*/
struct SerializeStaticShape
{
template
void operator()(S& serialize, const char* UNUSED(name), StaticShape& value)
{
serialize.NumberU32_Unbounded("entity", value.entity);
serialize.NumberFixed_Unbounded("x", value.x);
serialize.NumberFixed_Unbounded("z", value.z);
serialize.NumberFixed_Unbounded("u.x", value.u.X);
serialize.NumberFixed_Unbounded("u.y", value.u.Y);
serialize.NumberFixed_Unbounded("v.x", value.v.X);
serialize.NumberFixed_Unbounded("v.y", value.v.Y);
serialize.NumberFixed_Unbounded("hw", value.hw);
serialize.NumberFixed_Unbounded("hh", value.hh);
serialize.NumberU8_Unbounded("flags", value.flags);
serialize.NumberU32_Unbounded("group", value.group);
serialize.NumberU32_Unbounded("group2", value.group2);
}
};
class CCmpObstructionManager : public ICmpObstructionManager
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays
}
DEFAULT_COMPONENT_ALLOCATOR(ObstructionManager)
bool m_DebugOverlayEnabled;
bool m_DebugOverlayDirty;
std::vector m_DebugOverlayLines;
SpatialSubdivision m_UnitSubdivision;
SpatialSubdivision m_StaticSubdivision;
// TODO: using std::map is a bit inefficient; is there a better way to store these?
std::map m_UnitShapes;
std::map m_StaticShapes;
u32 m_UnitShapeNext; // next allocated id
u32 m_StaticShapeNext;
bool m_PassabilityCircular;
entity_pos_t m_WorldX0;
entity_pos_t m_WorldZ0;
entity_pos_t m_WorldX1;
entity_pos_t m_WorldZ1;
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_DebugOverlayEnabled = false;
m_DebugOverlayDirty = true;
m_UnitShapeNext = 1;
m_StaticShapeNext = 1;
m_DirtyID = 1; // init to 1 so default-initialised grids are considered dirty
m_PassabilityCircular = false;
m_WorldX0 = m_WorldZ0 = m_WorldX1 = m_WorldZ1 = entity_pos_t::Zero();
// Initialise with bogus values (these will get replaced when
// SetBounds is called)
ResetSubdivisions(entity_pos_t::FromInt(1), entity_pos_t::FromInt(1));
}
virtual void Deinit()
{
}
template
void SerializeCommon(S& serialize)
{
SerializeSpatialSubdivision()(serialize, "unit subdiv", m_UnitSubdivision);
SerializeSpatialSubdivision()(serialize, "static subdiv", m_StaticSubdivision);
SerializeMap()(serialize, "unit shapes", m_UnitShapes);
SerializeMap()(serialize, "static shapes", m_StaticShapes);
serialize.NumberU32_Unbounded("unit shape next", m_UnitShapeNext);
serialize.NumberU32_Unbounded("static shape next", m_StaticShapeNext);
serialize.Bool("circular", m_PassabilityCircular);
serialize.NumberFixed_Unbounded("world x0", m_WorldX0);
serialize.NumberFixed_Unbounded("world z0", m_WorldZ0);
serialize.NumberFixed_Unbounded("world x1", m_WorldX1);
serialize.NumberFixed_Unbounded("world z1", m_WorldZ1);
}
virtual void Serialize(ISerializer& serialize)
{
// TODO: this could perhaps be optimised by not storing all the obstructions,
// and instead regenerating them from the other entities on Deserialize
SerializeCommon(serialize);
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
SerializeCommon(deserialize);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_RenderSubmit:
{
const CMessageRenderSubmit& msgData = static_cast (msg);
RenderSubmit(msgData.collector);
break;
}
}
}
virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1)
{
m_WorldX0 = x0;
m_WorldZ0 = z0;
m_WorldX1 = x1;
m_WorldZ1 = z1;
MakeDirtyAll();
// Subdivision system bounds:
ENSURE(x0.IsZero() && z0.IsZero()); // don't bother implementing non-zero offsets yet
ResetSubdivisions(x1, z1);
}
void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1)
{
// Use 8x8 tile subdivisions
// (TODO: find the optimal number instead of blindly guessing)
m_UnitSubdivision.Reset(x1, z1, entity_pos_t::FromInt(8*TERRAIN_TILE_SIZE));
m_StaticSubdivision.Reset(x1, z1, entity_pos_t::FromInt(8*TERRAIN_TILE_SIZE));
for (std::map::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
CFixedVector2D center(it->second.x, it->second.z);
CFixedVector2D halfSize(it->second.r, it->second.r);
m_UnitSubdivision.Add(it->first, center - halfSize, center + halfSize);
}
for (std::map::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
CFixedVector2D center(it->second.x, it->second.z);
CFixedVector2D bbHalfSize = Geometry::GetHalfBoundingBox(it->second.u, it->second.v, CFixedVector2D(it->second.hw, it->second.hh));
m_StaticSubdivision.Add(it->first, center - bbHalfSize, center + bbHalfSize);
}
}
virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_pos_t r, flags_t flags, entity_id_t group)
{
UnitShape shape = { ent, x, z, r, flags, group };
u32 id = m_UnitShapeNext++;
m_UnitShapes[id] = shape;
MakeDirtyUnit(flags);
m_UnitSubdivision.Add(id, CFixedVector2D(x - r, z - r), CFixedVector2D(x + r, z + r));
return UNIT_INDEX_TO_TAG(id);
}
virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h, flags_t flags, entity_id_t group, entity_id_t group2 /* = INVALID_ENTITY */)
{
fixed s, c;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s);
CFixedVector2D v(s, c);
StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags, group, group2 };
u32 id = m_StaticShapeNext++;
m_StaticShapes[id] = shape;
MakeDirtyStatic(flags);
CFixedVector2D center(x, z);
CFixedVector2D bbHalfSize = Geometry::GetHalfBoundingBox(u, v, CFixedVector2D(w/2, h/2));
m_StaticSubdivision.Add(id, center - bbHalfSize, center + bbHalfSize);
return STATIC_INDEX_TO_TAG(id);
}
virtual ObstructionSquare GetUnitShapeObstruction(entity_pos_t x, entity_pos_t z, entity_pos_t r)
{
CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero());
CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1));
ObstructionSquare o = { x, z, u, v, r, r };
return o;
}
virtual ObstructionSquare GetStaticShapeObstruction(entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h)
{
fixed s, c;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s);
CFixedVector2D v(s, c);
ObstructionSquare o = { x, z, u, v, w/2, h/2 };
return o;
}
virtual void MoveShape(tag_t tag, entity_pos_t x, entity_pos_t z, entity_angle_t a)
{
ENSURE(TAG_IS_VALID(tag));
if (TAG_IS_UNIT(tag))
{
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
m_UnitSubdivision.Move(TAG_TO_INDEX(tag),
CFixedVector2D(shape.x - shape.r, shape.z - shape.r),
CFixedVector2D(shape.x + shape.r, shape.z + shape.r),
CFixedVector2D(x - shape.r, z - shape.r),
CFixedVector2D(x + shape.r, z + shape.r));
shape.x = x;
shape.z = z;
MakeDirtyUnit(shape.flags);
}
else
{
fixed s, c;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s);
CFixedVector2D v(s, c);
StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
CFixedVector2D fromBbHalfSize = Geometry::GetHalfBoundingBox(shape.u, shape.v, CFixedVector2D(shape.hw, shape.hh));
CFixedVector2D toBbHalfSize = Geometry::GetHalfBoundingBox(u, v, CFixedVector2D(shape.hw, shape.hh));
m_StaticSubdivision.Move(TAG_TO_INDEX(tag),
CFixedVector2D(shape.x, shape.z) - fromBbHalfSize,
CFixedVector2D(shape.x, shape.z) + fromBbHalfSize,
CFixedVector2D(x, z) - toBbHalfSize,
CFixedVector2D(x, z) + toBbHalfSize);
shape.x = x;
shape.z = z;
shape.u = u;
shape.v = v;
MakeDirtyStatic(shape.flags);
}
}
virtual void SetUnitMovingFlag(tag_t tag, bool moving)
{
ENSURE(TAG_IS_VALID(tag) && TAG_IS_UNIT(tag));
if (TAG_IS_UNIT(tag))
{
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
if (moving)
shape.flags |= FLAG_MOVING;
else
shape.flags &= (flags_t)~FLAG_MOVING;
MakeDirtyDebug();
}
}
virtual void SetUnitControlGroup(tag_t tag, entity_id_t group)
{
ENSURE(TAG_IS_VALID(tag) && TAG_IS_UNIT(tag));
if (TAG_IS_UNIT(tag))
{
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
shape.group = group;
}
}
virtual void SetStaticControlGroup(tag_t tag, entity_id_t group, entity_id_t group2)
{
ENSURE(TAG_IS_VALID(tag) && TAG_IS_STATIC(tag));
if (TAG_IS_STATIC(tag))
{
StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
shape.group = group;
shape.group2 = group2;
}
}
virtual void RemoveShape(tag_t tag)
{
ENSURE(TAG_IS_VALID(tag));
if (TAG_IS_UNIT(tag))
{
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
m_UnitSubdivision.Remove(TAG_TO_INDEX(tag),
CFixedVector2D(shape.x - shape.r, shape.z - shape.r),
CFixedVector2D(shape.x + shape.r, shape.z + shape.r));
MakeDirtyUnit(shape.flags);
m_UnitShapes.erase(TAG_TO_INDEX(tag));
}
else
{
StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
CFixedVector2D center(shape.x, shape.z);
CFixedVector2D bbHalfSize = Geometry::GetHalfBoundingBox(shape.u, shape.v, CFixedVector2D(shape.hw, shape.hh));
m_StaticSubdivision.Remove(TAG_TO_INDEX(tag), center - bbHalfSize, center + bbHalfSize);
MakeDirtyStatic(shape.flags);
m_StaticShapes.erase(TAG_TO_INDEX(tag));
}
}
virtual ObstructionSquare GetObstruction(tag_t tag)
{
ENSURE(TAG_IS_VALID(tag));
if (TAG_IS_UNIT(tag))
{
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero());
CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1));
ObstructionSquare o = { shape.x, shape.z, u, v, shape.r, shape.r };
return o;
}
else
{
StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
ObstructionSquare o = { shape.x, shape.z, shape.u, shape.v, shape.hw, shape.hh };
return o;
}
}
virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r);
virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out);
virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out);
virtual bool Rasterise(Grid& grid);
virtual void GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector& squares);
virtual bool FindMostImportantObstruction(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, ObstructionSquare& square);
virtual void SetPassabilityCircular(bool enabled)
{
m_PassabilityCircular = enabled;
MakeDirtyAll();
}
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlayEnabled = enabled;
m_DebugOverlayDirty = true;
if (!enabled)
m_DebugOverlayLines.clear();
}
void RenderSubmit(SceneCollector& collector);
private:
// To support lazy updates of grid rasterisations of obstruction data,
// we maintain a DirtyID here and increment it whenever obstructions change;
// if a grid has a lower DirtyID then it needs to be updated.
size_t m_DirtyID;
/**
* Mark all previous Rasterise()d grids as dirty, and the debug display.
* Call this when the world bounds have changed.
*/
void MakeDirtyAll()
{
++m_DirtyID;
m_DebugOverlayDirty = true;
}
/**
* Mark the debug display as dirty.
* Call this when nothing has changed except a unit's 'moving' flag.
*/
void MakeDirtyDebug()
{
m_DebugOverlayDirty = true;
}
/**
* Mark all previous Rasterise()d grids as dirty, if they depend on this shape.
* Call this when a static shape has changed.
*/
void MakeDirtyStatic(flags_t flags)
{
if (flags & (FLAG_BLOCK_PATHFINDING|FLAG_BLOCK_FOUNDATION))
++m_DirtyID;
m_DebugOverlayDirty = true;
}
/**
* Mark all previous Rasterise()d grids as dirty, if they depend on this shape.
* Call this when a unit shape has changed.
*/
void MakeDirtyUnit(flags_t flags)
{
if (flags & (FLAG_BLOCK_PATHFINDING|FLAG_BLOCK_FOUNDATION))
++m_DirtyID;
m_DebugOverlayDirty = true;
}
/**
* Test whether a Rasterise()d grid is dirty and needs updating
*/
template
bool IsDirty(const Grid& grid)
{
return grid.m_DirtyID < m_DirtyID;
}
/**
* Return whether the given point is within the world bounds by at least r
*/
bool IsInWorld(entity_pos_t x, entity_pos_t z, entity_pos_t r)
{
return (m_WorldX0+r <= x && x <= m_WorldX1-r && m_WorldZ0+r <= z && z <= m_WorldZ1-r);
}
/**
* Return whether the given point is within the world bounds
*/
bool IsInWorld(CFixedVector2D p)
{
return (m_WorldX0 <= p.X && p.X <= m_WorldX1 && m_WorldZ0 <= p.Y && p.Y <= m_WorldZ1);
}
};
REGISTER_COMPONENT_TYPE(ObstructionManager)
bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r)
{
PROFILE("TestLine");
// Check that both end points are within the world (which means the whole line must be)
if (!IsInWorld(x0, z0, r) || !IsInWorld(x1, z1, r))
return true;
CFixedVector2D posMin (std::min(x0, x1) - r, std::min(z0, z1) - r);
CFixedVector2D posMax (std::max(x0, x1) + r, std::max(z0, z1) + r);
std::vector unitShapes = m_UnitSubdivision.GetInRange(posMin, posMax);
for (size_t i = 0; i < unitShapes.size(); ++i)
{
std::map::iterator it = m_UnitShapes.find(unitShapes[i]);
ENSURE(it != m_UnitShapes.end());
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
continue;
CFixedVector2D center(it->second.x, it->second.z);
CFixedVector2D halfSize(it->second.r + r, it->second.r + r);
if (Geometry::TestRayAASquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, halfSize))
return true;
}
std::vector staticShapes = m_StaticSubdivision.GetInRange(posMin, posMax);
for (size_t i = 0; i < staticShapes.size(); ++i)
{
std::map::iterator it = m_StaticShapes.find(staticShapes[i]);
ENSURE(it != m_StaticShapes.end());
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
continue;
CFixedVector2D center(it->second.x, it->second.z);
CFixedVector2D halfSize(it->second.hw + r, it->second.hh + r);
if (Geometry::TestRaySquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, it->second.u, it->second.v, halfSize))
return true;
}
return false;
}
bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filter,
entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h,
std::vector* out)
{
PROFILE("TestStaticShape");
// TODO: should use the subdivision stuff here, if performance is non-negligible
if (out)
out->clear();
fixed s, c;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s);
CFixedVector2D v(s, c);
CFixedVector2D center(x, z);
CFixedVector2D halfSize(w/2, h/2);
// Check that all corners are within the world (which means the whole shape must be)
if (!IsInWorld(center + u.Multiply(halfSize.X) + v.Multiply(halfSize.Y)) ||
!IsInWorld(center + u.Multiply(halfSize.X) - v.Multiply(halfSize.Y)) ||
!IsInWorld(center - u.Multiply(halfSize.X) + v.Multiply(halfSize.Y)) ||
!IsInWorld(center - u.Multiply(halfSize.X) - v.Multiply(halfSize.Y)))
{
if (out)
out->push_back(INVALID_ENTITY); // no entity ID, so just push an arbitrary marker
else
return true;
}
for (std::map::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
if (Geometry::PointIsInSquare(center1 - center, u, v, CFixedVector2D(halfSize.X + it->second.r, halfSize.Y + it->second.r)))
{
if (out)
out->push_back(it->second.entity);
else
return true;
}
}
for (std::map::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
CFixedVector2D halfSize1(it->second.hw, it->second.hh);
if (Geometry::TestSquareSquare(center, u, v, halfSize, center1, it->second.u, it->second.v, halfSize1))
{
if (out)
out->push_back(it->second.entity);
else
return true;
}
}
if (out)
return !out->empty(); // collided if the list isn't empty
else
return false; // didn't collide, if we got this far
}
bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,
entity_pos_t x, entity_pos_t z, entity_pos_t r,
std::vector* out)
{
PROFILE("TestUnitShape");
// TODO: should use the subdivision stuff here, if performance is non-negligible
// Check that the shape is within the world
if (!IsInWorld(x, z, r))
{
if (out)
out->push_back(INVALID_ENTITY); // no entity ID, so just push an arbitrary marker
else
return true;
}
CFixedVector2D center(x, z);
for (std::map::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
continue;
entity_pos_t r1 = it->second.r;
if (!(it->second.x + r1 < x - r || it->second.x - r1 > x + r || it->second.z + r1 < z - r || it->second.z - r1 > z + r))
{
if (out)
out->push_back(it->second.entity);
else
return true;
}
}
for (std::map::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
if (Geometry::PointIsInSquare(center1 - center, it->second.u, it->second.v, CFixedVector2D(it->second.hw + r, it->second.hh + r)))
{
if (out)
out->push_back(it->second.entity);
else
return true;
}
}
if (out)
return !out->empty(); // collided if the list isn't empty
else
return false; // didn't collide, if we got this far
}
/**
* Compute the tile indexes on the grid nearest to a given point
*/
static void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h)
{
i = (u16)clamp((x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, w-1);
j = (u16)clamp((z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, h-1);
}
/**
* Returns the position of the center of the given tile
*/
static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
x = entity_pos_t::FromInt(i*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE/2);
z = entity_pos_t::FromInt(j*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE/2);
}
bool CCmpObstructionManager::Rasterise(Grid& grid)
{
if (!IsDirty(grid))
return false;
PROFILE("Rasterise");
grid.m_DirtyID = m_DirtyID;
// TODO: this is all hopelessly inefficient
// What we should perhaps do is have some kind of quadtree storing Shapes so it's
// quick to invalidate and update small numbers of tiles
grid.reset();
// For tile-based pathfinding:
// Since we only count tiles whose centers are inside the square,
// we maybe want to expand the square a bit so we're less likely to think there's
// free space between buildings when there isn't. But this is just a random guess
// and needs to be tweaked until everything works nicely.
//entity_pos_t expandPathfinding = entity_pos_t::FromInt(TERRAIN_TILE_SIZE / 2);
// Actually that's bad because units get stuck when the A* pathfinder thinks they're
// blocked on all sides, so it's better to underestimate
entity_pos_t expandPathfinding = entity_pos_t::FromInt(0);
// For AI building foundation planning, we want to definitely block all
// potentially-obstructed tiles (so we don't blindly build on top of an obstruction),
// so we need to expand by at least 1/sqrt(2) of a tile
entity_pos_t expandFoundation = (entity_pos_t::FromInt(TERRAIN_TILE_SIZE) * 3) / 4;
for (std::map::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
CFixedVector2D center(it->second.x, it->second.z);
if (it->second.flags & FLAG_BLOCK_PATHFINDING)
{
CFixedVector2D halfSize(it->second.hw + expandPathfinding, it->second.hh + expandPathfinding);
CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(it->second.u, it->second.v, halfSize);
u16 i0, j0, i1, j1;
NearestTile(center.X - halfBound.X, center.Y - halfBound.Y, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + halfBound.X, center.Y + halfBound.Y, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
{
for (u16 i = i0; i <= i1; ++i)
{
entity_pos_t x, z;
TileCenter(i, j, x, z);
if (Geometry::PointIsInSquare(CFixedVector2D(x, z) - center, it->second.u, it->second.v, halfSize))
grid.set(i, j, grid.get(i, j) | TILE_OBSTRUCTED_PATHFINDING);
}
}
}
if (it->second.flags & FLAG_BLOCK_FOUNDATION)
{
CFixedVector2D halfSize(it->second.hw + expandFoundation, it->second.hh + expandFoundation);
CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(it->second.u, it->second.v, halfSize);
u16 i0, j0, i1, j1;
NearestTile(center.X - halfBound.X, center.Y - halfBound.Y, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + halfBound.X, center.Y + halfBound.Y, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
{
for (u16 i = i0; i <= i1; ++i)
{
entity_pos_t x, z;
TileCenter(i, j, x, z);
if (Geometry::PointIsInSquare(CFixedVector2D(x, z) - center, it->second.u, it->second.v, halfSize))
grid.set(i, j, grid.get(i, j) | TILE_OBSTRUCTED_FOUNDATION);
}
}
}
}
for (std::map::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
CFixedVector2D center(it->second.x, it->second.z);
if (it->second.flags & FLAG_BLOCK_PATHFINDING)
{
entity_pos_t r = it->second.r + expandPathfinding;
u16 i0, j0, i1, j1;
NearestTile(center.X - r, center.Y - r, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + r, center.Y + r, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
for (u16 i = i0; i <= i1; ++i)
grid.set(i, j, grid.get(i, j) | TILE_OBSTRUCTED_PATHFINDING);
}
if (it->second.flags & FLAG_BLOCK_FOUNDATION)
{
entity_pos_t r = it->second.r + expandFoundation;
u16 i0, j0, i1, j1;
NearestTile(center.X - r, center.Y - r, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + r, center.Y + r, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
for (u16 i = i0; i <= i1; ++i)
grid.set(i, j, grid.get(i, j) | TILE_OBSTRUCTED_FOUNDATION);
}
}
// Any tiles outside or very near the edge of the map are impassable
// WARNING: CCmpRangeManager::LosIsOffWorld needs to be kept in sync with this
const u16 edgeSize = 3; // number of tiles around the edge that will be off-world
u8 edgeFlags = TILE_OBSTRUCTED_PATHFINDING | TILE_OBSTRUCTED_FOUNDATION | TILE_OUTOFBOUNDS;
if (m_PassabilityCircular)
{
for (u16 j = 0; j < grid.m_H; ++j)
{
for (u16 i = 0; i < grid.m_W; ++i)
{
// Based on CCmpRangeManager::LosIsOffWorld
// but tweaked since it's tile-based instead.
// (We double all the values so we can handle half-tile coordinates.)
// This needs to be slightly tighter than the LOS circle,
// else units might get themselves lost in the SoD around the edge.
ssize_t dist2 = (i*2 + 1 - grid.m_W)*(i*2 + 1 - grid.m_W)
+ (j*2 + 1 - grid.m_H)*(j*2 + 1 - grid.m_H);
if (dist2 >= (grid.m_W - 2*edgeSize) * (grid.m_H - 2*edgeSize))
grid.set(i, j, edgeFlags);
}
}
}
else
{
u16 i0, j0, i1, j1;
NearestTile(m_WorldX0, m_WorldZ0, i0, j0, grid.m_W, grid.m_H);
NearestTile(m_WorldX1, m_WorldZ1, i1, j1, grid.m_W, grid.m_H);
for (u16 j = 0; j < grid.m_H; ++j)
for (u16 i = 0; i < i0+edgeSize; ++i)
grid.set(i, j, edgeFlags);
for (u16 j = 0; j < grid.m_H; ++j)
for (u16 i = (u16)(i1-edgeSize+1); i < grid.m_W; ++i)
grid.set(i, j, edgeFlags);
for (u16 j = 0; j < j0+edgeSize; ++j)
for (u16 i = (u16)(i0+edgeSize); i < i1-edgeSize+1; ++i)
grid.set(i, j, edgeFlags);
for (u16 j = (u16)(j1-edgeSize+1); j < grid.m_H; ++j)
for (u16 i = (u16)(i0+edgeSize); i < i1-edgeSize+1; ++i)
grid.set(i, j, edgeFlags);
}
return true;
}
void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector& squares)
{
PROFILE("GetObstructionsInRange");
ENSURE(x0 <= x1 && z0 <= z1);
std::vector unitShapes = m_UnitSubdivision.GetInRange(CFixedVector2D(x0, z0), CFixedVector2D(x1, z1));
for (size_t i = 0; i < unitShapes.size(); ++i)
{
std::map::iterator it = m_UnitShapes.find(unitShapes[i]);
ENSURE(it != m_UnitShapes.end());
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
continue;
entity_pos_t r = it->second.r;
// Skip this object if it's completely outside the requested range
if (it->second.x + r < x0 || it->second.x - r > x1 || it->second.z + r < z0 || it->second.z - r > z1)
continue;
CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero());
CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1));
ObstructionSquare s = { it->second.x, it->second.z, u, v, r, r };
squares.push_back(s);
}
std::vector staticShapes = m_StaticSubdivision.GetInRange(CFixedVector2D(x0, z0), CFixedVector2D(x1, z1));
for (size_t i = 0; i < staticShapes.size(); ++i)
{
std::map::iterator it = m_StaticShapes.find(staticShapes[i]);
ENSURE(it != m_StaticShapes.end());
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
continue;
entity_pos_t r = it->second.hw + it->second.hh; // overestimate the max dist of an edge from the center
// Skip this object if its overestimated bounding box is completely outside the requested range
if (it->second.x + r < x0 || it->second.x - r > x1 || it->second.z + r < z0 || it->second.z - r > z1)
continue;
// TODO: maybe we should use Geometry::GetHalfBoundingBox to be more precise?
ObstructionSquare s = { it->second.x, it->second.z, it->second.u, it->second.v, it->second.hw, it->second.hh };
squares.push_back(s);
}
}
bool CCmpObstructionManager::FindMostImportantObstruction(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, ObstructionSquare& square)
{
std::vector squares;
CFixedVector2D center(x, z);
// First look for obstructions that are covering the exact target point
GetObstructionsInRange(filter, x, z, x, z, squares);
// Building squares are more important but returned last, so check backwards
for (std::vector::reverse_iterator it = squares.rbegin(); it != squares.rend(); ++it)
{
CFixedVector2D halfSize(it->hw, it->hh);
if (Geometry::PointIsInSquare(CFixedVector2D(it->x, it->z) - center, it->u, it->v, halfSize))
{
square = *it;
return true;
}
}
// Then look for obstructions that cover the target point when expanded by r
// (i.e. if the target is not inside an object but closer than we can get to it)
-
- // TODO: actually do that
- // (This might matter when you tell a unit to walk too close to the edge of a building)
+
+ GetObstructionsInRange(filter, x-r, z-r, x+r, z+r, squares);
+ // Building squares are more important but returned last, so check backwards
+ for (std::vector::reverse_iterator it = squares.rbegin(); it != squares.rend(); ++it)
+ {
+ CFixedVector2D halfSize(it->hw + r, it->hh + r);
+ if (Geometry::PointIsInSquare(CFixedVector2D(it->x, it->z) - center, it->u, it->v, halfSize))
+ {
+ square = *it;
+ return true;
+ }
+ }
return false;
}
void CCmpObstructionManager::RenderSubmit(SceneCollector& collector)
{
if (!m_DebugOverlayEnabled)
return;
CColor defaultColour(0, 0, 1, 1);
CColor movingColour(1, 0, 1, 1);
CColor boundsColour(1, 1, 0, 1);
// If the shapes have changed, then regenerate all the overlays
if (m_DebugOverlayDirty)
{
m_DebugOverlayLines.clear();
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = boundsColour;
SimRender::ConstructSquareOnGround(GetSimContext(),
(m_WorldX0+m_WorldX1).ToFloat()/2.f, (m_WorldZ0+m_WorldZ1).ToFloat()/2.f,
(m_WorldX1-m_WorldX0).ToFloat(), (m_WorldZ1-m_WorldZ0).ToFloat(),
0, m_DebugOverlayLines.back(), true);
for (std::map::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = ((it->second.flags & FLAG_MOVING) ? movingColour : defaultColour);
SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.r.ToFloat()*2, it->second.r.ToFloat()*2, 0, m_DebugOverlayLines.back(), true);
}
for (std::map::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = defaultColour;
float a = atan2f(it->second.v.X.ToFloat(), it->second.v.Y.ToFloat());
SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.hw.ToFloat()*2, it->second.hh.ToFloat()*2, a, m_DebugOverlayLines.back(), true);
}
m_DebugOverlayDirty = false;
}
for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i)
collector.Submit(&m_DebugOverlayLines[i]);
}
Index: ps/trunk/source/simulation2/components/tests/test_RangeManager.h
===================================================================
--- ps/trunk/source/simulation2/components/tests/test_RangeManager.h (revision 11885)
+++ ps/trunk/source/simulation2/components/tests/test_RangeManager.h (revision 11886)
@@ -1,127 +1,129 @@
/* Copyright (C) 2011 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 "simulation2/system/ComponentTest.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpVision.h"
#include "maths/Random.h"
#include
class MockVision : public ICmpVision
{
public:
DEFAULT_MOCK_COMPONENT()
virtual entity_pos_t GetRange() { return entity_pos_t::FromInt(66); }
virtual bool GetRetainInFog() { return false; }
virtual bool GetAlwaysVisible() { return false; }
};
class MockPosition : public ICmpPosition
{
public:
DEFAULT_MOCK_COMPONENT()
virtual bool IsInWorld() { return true; }
virtual void MoveOutOfWorld() { }
virtual void MoveTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) { }
virtual void JumpTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) { }
virtual void SetHeightOffset(entity_pos_t UNUSED(dy)) { }
virtual entity_pos_t GetHeightOffset() { return entity_pos_t::Zero(); }
virtual void SetHeightFixed(entity_pos_t UNUSED(y)) { }
virtual bool IsFloating() { return false; }
virtual CFixedVector3D GetPosition() { return CFixedVector3D(); }
virtual CFixedVector2D GetPosition2D() { return CFixedVector2D(); }
+ virtual CFixedVector3D GetPreviousPosition() { return CFixedVector3D(); }
+ virtual CFixedVector2D GetPreviousPosition2D() { return CFixedVector2D(); }
virtual void TurnTo(entity_angle_t UNUSED(y)) { }
virtual void SetYRotation(entity_angle_t UNUSED(y)) { }
virtual void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) { }
virtual CFixedVector3D GetRotation() { return CFixedVector3D(); }
virtual fixed GetDistanceTravelled() { return fixed::Zero(); }
virtual void GetInterpolatedPosition2D(float UNUSED(frameOffset), float& x, float& z, float& rotY) { x = z = rotY = 0; }
virtual CMatrix3D GetInterpolatedTransform(float UNUSED(frameOffset), bool UNUSED(forceFloating)) { return CMatrix3D(); }
};
class TestCmpRangeManager : public CxxTest::TestSuite
{
public:
void setUp()
{
CXeromyces::Startup();
}
void tearDown()
{
CXeromyces::Terminate();
}
void test_basic()
{
ComponentTestHelper test;
ICmpRangeManager* cmp = test.Add(CID_RangeManager, "");
MockVision vision;
test.AddMock(100, IID_Vision, vision);
MockPosition position;
test.AddMock(100, IID_Position, position);
// This tests that the incremental computation produces the correct result
// in various edge cases
cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512), 512/TERRAIN_TILE_SIZE + 1);
cmp->Verify();
{ CMessageCreate msg(100); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
WELL512 rng;
for (size_t i = 0; i < 1024; ++i)
{
double x = boost::uniform_real<>(0.0, 512.0)(rng);
double z = boost::uniform_real<>(0.0, 512.0)(rng);
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromDouble(x), entity_pos_t::FromDouble(z), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
cmp->Verify();
}
}
};
Index: ps/trunk/source/simulation2/components/CCmpPosition.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpPosition.cpp (revision 11885)
+++ ps/trunk/source/simulation2/components/CCmpPosition.cpp (revision 11886)
@@ -1,435 +1,478 @@
/* Copyright (C) 2012 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 "ICmpPosition.h"
#include "simulation2/MessageTypes.h"
#include "ICmpTerrain.h"
#include "ICmpWaterManager.h"
#include "graphics/Terrain.h"
#include "lib/rand.h"
#include "maths/MathUtil.h"
#include "maths/Matrix3D.h"
#include "maths/Vector3D.h"
#include "ps/CLogger.h"
/**
* Basic ICmpPosition implementation.
*/
class CCmpPosition : public ICmpPosition
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_TurnStart);
componentManager.SubscribeToMessageType(MT_Interpolate);
// TODO: if this component turns out to be a performance issue, it should
// be optimised by creating a new PositionStatic component that doesn't subscribe
// to messages and doesn't store LastX/LastZ, and that should be used for all
// entities that don't move
}
DEFAULT_COMPONENT_ALLOCATOR(Position)
// Template state:
enum
{
UPRIGHT = 0,
PITCH = 1,
PITCH_ROLL = 2,
} m_AnchorType;
bool m_Floating;
float m_RotYSpeed; // maximum radians per second, used by InterpolatedRotY to follow RotY
// Dynamic state:
bool m_InWorld;
- entity_pos_t m_X, m_Z, m_LastX, m_LastZ; // these values contain undefined junk if !InWorld
+ // m_LastX/Z contain the position from the start of the most recent turn
+ // m_PrevX/Z conatain the position from the turn before that
+ entity_pos_t m_X, m_Z, m_LastX, m_LastZ, m_PrevX, m_PrevZ; // these values contain undefined junk if !InWorld
entity_pos_t m_YOffset;
bool m_RelativeToGround; // whether m_YOffset is relative to terrain/water plane, or an absolute height
entity_angle_t m_RotX, m_RotY, m_RotZ;
float m_InterpolatedRotY; // not serialized
static std::string GetSchema()
{
return
"Allows this entity to exist at a location (and orientation) in the world, and defines some details of the positioning."
""
"upright"
"0.0"
"false"
"6.0"
""
""
""
"upright"
"pitch"
"pitch-roll"
""
""
""
""
""
""
""
""
""
""
"";
}
virtual void Init(const CParamNode& paramNode)
{
std::wstring anchor = paramNode.GetChild("Anchor").ToString();
if (anchor == L"pitch")
m_AnchorType = PITCH;
else if (anchor == L"pitch-roll")
m_AnchorType = PITCH_ROLL;
else
m_AnchorType = UPRIGHT;
m_InWorld = false;
m_YOffset = paramNode.GetChild("Altitude").ToFixed();
m_RelativeToGround = true;
m_Floating = paramNode.GetChild("Floating").ToBool();
m_RotYSpeed = paramNode.GetChild("TurnRate").ToFixed().ToFloat();
m_RotX = m_RotY = m_RotZ = entity_angle_t::FromInt(0);
m_InterpolatedRotY = 0;
}
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& serialize)
{
serialize.Bool("in world", m_InWorld);
if (m_InWorld)
{
serialize.NumberFixed_Unbounded("x", m_X);
serialize.NumberFixed_Unbounded("z", m_Z);
serialize.NumberFixed_Unbounded("last x", m_LastX);
serialize.NumberFixed_Unbounded("last z", m_LastZ);
// TODO: for efficiency, we probably shouldn't actually store the last position - it doesn't
// matter if we don't have smooth interpolation after reloading a game
}
serialize.NumberFixed_Unbounded("rot x", m_RotX);
serialize.NumberFixed_Unbounded("rot y", m_RotY);
serialize.NumberFixed_Unbounded("rot z", m_RotZ);
serialize.NumberFixed_Unbounded("altitude", m_YOffset);
serialize.Bool("relative", m_RelativeToGround);
if (serialize.IsDebug())
{
const char* anchor = "???";
switch (m_AnchorType)
{
case UPRIGHT: anchor = "upright"; break;
case PITCH: anchor = "pitch"; break;
case PITCH_ROLL: anchor = "pitch-roll"; break;
}
serialize.StringASCII("anchor", anchor, 0, 16);
serialize.Bool("floating", m_Floating);
}
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
deserialize.Bool("in world", m_InWorld);
if (m_InWorld)
{
deserialize.NumberFixed_Unbounded("x", m_X);
deserialize.NumberFixed_Unbounded("z", m_Z);
deserialize.NumberFixed_Unbounded("last x", m_LastX);
deserialize.NumberFixed_Unbounded("last z", m_LastZ);
}
deserialize.NumberFixed_Unbounded("rot x", m_RotX);
deserialize.NumberFixed_Unbounded("rot y", m_RotY);
deserialize.NumberFixed_Unbounded("rot z", m_RotZ);
deserialize.NumberFixed_Unbounded("altitude", m_YOffset);
deserialize.Bool("relative", m_RelativeToGround);
// TODO: should there be range checks on all these values?
m_InterpolatedRotY = m_RotY.ToFloat();
}
virtual bool IsInWorld()
{
return m_InWorld;
}
virtual void MoveOutOfWorld()
{
m_InWorld = false;
AdvertisePositionChanges();
}
virtual void MoveTo(entity_pos_t x, entity_pos_t z)
{
m_X = x;
m_Z = z;
if (!m_InWorld)
{
m_InWorld = true;
- m_LastX = m_X;
- m_LastZ = m_Z;
+ m_LastX = m_PrevX = m_X;
+ m_LastZ = m_PrevZ = m_Z;
}
AdvertisePositionChanges();
}
virtual void JumpTo(entity_pos_t x, entity_pos_t z)
{
- m_LastX = m_X = x;
- m_LastZ = m_Z = z;
+ m_LastX = m_PrevX = m_X = x;
+ m_LastZ = m_PrevZ = m_Z = z;
m_InWorld = true;
AdvertisePositionChanges();
}
virtual void SetHeightOffset(entity_pos_t dy)
{
m_YOffset = dy;
m_RelativeToGround = true;
AdvertisePositionChanges();
}
virtual entity_pos_t GetHeightOffset()
{
return m_YOffset;
}
virtual void SetHeightFixed(entity_pos_t y)
{
m_YOffset = y;
m_RelativeToGround = false;
}
virtual bool IsFloating()
{
return m_Floating;
}
virtual CFixedVector3D GetPosition()
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::GetPosition called on entity when IsInWorld is false");
return CFixedVector3D();
}
entity_pos_t baseY;
if (m_RelativeToGround)
{
CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
if (cmpTerrain)
baseY = cmpTerrain->GetGroundLevel(m_X, m_Z);
if (m_Floating)
{
CmpPtr cmpWaterManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpWaterManager)
baseY = std::max(baseY, cmpWaterManager->GetWaterLevel(m_X, m_Z));
}
}
return CFixedVector3D(m_X, baseY + m_YOffset, m_Z);
}
virtual CFixedVector2D GetPosition2D()
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::GetPosition2D called on entity when IsInWorld is false");
return CFixedVector2D();
}
return CFixedVector2D(m_X, m_Z);
}
+ virtual CFixedVector3D GetPreviousPosition()
+ {
+ if (!m_InWorld)
+ {
+ LOGERROR(L"CCmpPosition::GetPreviousPosition called on entity when IsInWorld is false");
+ return CFixedVector3D();
+ }
+
+ entity_pos_t baseY;
+ if (m_RelativeToGround)
+ {
+ CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
+ if (cmpTerrain)
+ baseY = cmpTerrain->GetGroundLevel(m_PrevX, m_PrevZ);
+
+ if (m_Floating)
+ {
+ CmpPtr cmpWaterMan(GetSimContext(), SYSTEM_ENTITY);
+ if (cmpWaterMan)
+ baseY = std::max(baseY, cmpWaterMan->GetWaterLevel(m_PrevX, m_PrevZ));
+ }
+ }
+
+ return CFixedVector3D(m_PrevX, baseY + m_YOffset, m_PrevZ);
+ }
+
+ virtual CFixedVector2D GetPreviousPosition2D()
+ {
+ if (!m_InWorld)
+ {
+ LOGERROR(L"CCmpPosition::GetPreviousPosition2D called on entity when IsInWorld is false");
+ return CFixedVector2D();
+ }
+
+ return CFixedVector2D(m_PrevX, m_PrevZ);
+ }
+
virtual void TurnTo(entity_angle_t y)
{
m_RotY = y;
AdvertisePositionChanges();
}
virtual void SetYRotation(entity_angle_t y)
{
m_RotY = y;
m_InterpolatedRotY = m_RotY.ToFloat();
AdvertisePositionChanges();
}
virtual void SetXZRotation(entity_angle_t x, entity_angle_t z)
{
m_RotX = x;
m_RotZ = z;
AdvertisePositionChanges();
}
virtual CFixedVector3D GetRotation()
{
return CFixedVector3D(m_RotX, m_RotY, m_RotZ);
}
virtual fixed GetDistanceTravelled()
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::GetDistanceTravelled called on entity when IsInWorld is false");
return fixed::Zero();
}
return CFixedVector2D(m_X - m_LastX, m_Z - m_LastZ).Length();
}
virtual void GetInterpolatedPosition2D(float frameOffset, float& x, float& z, float& rotY)
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::GetInterpolatedPosition2D called on entity when IsInWorld is false");
return;
}
x = Interpolate(m_LastX.ToFloat(), m_X.ToFloat(), frameOffset);
z = Interpolate(m_LastZ.ToFloat(), m_Z.ToFloat(), frameOffset);
rotY = m_InterpolatedRotY;
}
virtual CMatrix3D GetInterpolatedTransform(float frameOffset, bool forceFloating)
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::GetInterpolatedTransform called on entity when IsInWorld is false");
CMatrix3D m;
m.SetIdentity();
return m;
}
float x, z, rotY;
GetInterpolatedPosition2D(frameOffset, x, z, rotY);
float baseY = 0;
if (m_RelativeToGround)
{
CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
if (cmpTerrain)
baseY = cmpTerrain->GetExactGroundLevel(x, z);
if (m_Floating || forceFloating)
{
CmpPtr cmpWaterManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpWaterManager)
baseY = std::max(baseY, cmpWaterManager->GetExactWaterLevel(x, z));
}
}
float y = baseY + m_YOffset.ToFloat();
// TODO: do something with m_AnchorType
CMatrix3D m;
CMatrix3D mXZ;
float Cos = cosf(rotY);
float Sin = sinf(rotY);
m.SetIdentity();
m._11 = -Cos;
m._13 = -Sin;
m._31 = Sin;
m._33 = -Cos;
mXZ.SetIdentity();
mXZ.SetXRotation(m_RotX.ToFloat());
mXZ.RotateZ(m_RotZ.ToFloat());
// TODO: is this all done in the correct order?
mXZ = m * mXZ;
mXZ.Translate(CVector3D(x, y, z));
return mXZ;
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Interpolate:
{
const CMessageInterpolate& msgData = static_cast (msg);
float rotY = m_RotY.ToFloat();
float delta = rotY - m_InterpolatedRotY;
// Wrap delta to -M_PI..M_PI
delta = fmodf(delta + (float)M_PI, 2*(float)M_PI); // range -2PI..2PI
if (delta < 0) delta += 2*(float)M_PI; // range 0..2PI
delta -= (float)M_PI; // range -M_PI..M_PI
// Clamp to max rate
float deltaClamped = clamp(delta, -m_RotYSpeed*msgData.frameTime, +m_RotYSpeed*msgData.frameTime);
// Calculate new orientation, in a peculiar way in order to make sure the
// result gets close to m_orientation (rather than being n*2*M_PI out)
m_InterpolatedRotY = rotY + deltaClamped - delta;
break;
}
case MT_TurnStart:
{
+ // Store the positions from the turn before
+ m_PrevX = m_LastX;
+ m_PrevZ = m_LastZ;
+
m_LastX = m_X;
m_LastZ = m_Z;
break;
}
}
}
private:
void AdvertisePositionChanges()
{
if (m_InWorld)
{
CMessagePositionChanged msg(GetEntityId(), true, m_X, m_Z, m_RotY);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
else
{
CMessagePositionChanged msg(GetEntityId(), false, entity_pos_t::Zero(), entity_pos_t::Zero(), entity_angle_t::Zero());
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
}
};
REGISTER_COMPONENT_TYPE(Position)
Index: ps/trunk/source/simulation2/docs/SimulationDocs.h
===================================================================
--- ps/trunk/source/simulation2/docs/SimulationDocs.h (revision 11885)
+++ ps/trunk/source/simulation2/docs/SimulationDocs.h (revision 11886)
@@ -1,628 +1,629 @@
/* 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 .
*/
/**
@page writing-components How to write components
See the Trac wiki for more documentation about this system.
- @ref defining-cpp-interfaces
- @ref script-wrapper
- @ref script-conversions
- @ref defining-cpp-components
- @ref messages
- @ref component-creation
- @ref schema
- @ref allowing-js-interfaces
- @ref defining-js-components
- @ref defining-js-interfaces
- @ref defining-cpp-message
- @ref defining-js-message
- @ref communication
- @ref message-passing
- @ref query-interface
- @ref testing
- @ref testing-cpp
- @ref testing-js
@section defining-cpp-interfaces Defining interfaces in C++
Think of a name for the component. We'll use "Example" in this example; replace
it with your chosen name in all the filenames and code samples below.
(If you copy-and-paste from the examples below, be aware that the
coding conventions
require indentation with tabs, not spaces, so make sure you get it right.)
Create the file @b simulation2/components/ICmpExample.h:
@include ICmpExample.h
This defines the interface that C++ code will use to access components.
Create the file @b simulation2/components/ICmpExample.cpp:
@include ICmpExample.cpp
This defines a JavaScript wrapper, so that scripts can access methods of components
implementing that interface. See @ref script-wrapper for details.
This wrapper should only contain methods that are safe to access from simulation scripts:
they must not crash (even with invalid or malicious inputs), they must return deterministic
results, etc.
Methods that are intended for use solely by C++ should not be listed here.
Every interface must define a script wrapper with @c BEGIN_INTERFACE_WRAPPER,
though in some cases they might be empty and not define any methods.
Now update the file simulation2/TypeList.h and add
@code
INTERFACE(Example)
@endcode
TypeList.h is used for various purposes - it will define the interface ID number @c IID_Example
(in both C++ and JS), and it will hook the new interface into the interface registration system.
Remember to run the @c update-workspaces script after adding or removing any source files,
so that they will be added to the makefiles or VS projects.
@section script-wrapper Interface method script wrappers
Interface methods are defined with the macro:
DEFINE_INTERFACE_METHOD_NumberOfArguments("MethodName",
ReturnType, ICmpExample, MethodName, ArgType0, ArgType1, ...)
corresponding to the C++ method
ReturnType ICmpExample::MethodName(ArgType0, ArgType1, ...)
For methods exposed to scripts like this, the arguments should be simple types and pass-by-value.
E.g. use std::wstring arguments, not const std::wstring&.
The arguments and return types will be automatically converted between C++ and JS values.
To do this, @c ToJSVal and @c FromJSVal must be defined (if they
haven't already been defined for another method), as described below.
The two MethodNames don't have to be the same - in rare cases you might want to expose it as
@c DoWhatever to scripts but link it to the @c ICmpExample::DoWhatever_wrapper() method
which does some extra conversions or checks or whatever.
There's a small limit to the number of arguments that are currently supported - if you need more,
first try to save yourself some pain by using fewer arguments, otherwise you'll need to add a new
macro into simulation2/system/InterfaceScripted.h and increase @ref SCRIPT_INTERFACE_MAX_ARGS in scriptinterface/ScriptInterface.h.
(Not sure if anything else needs changing.)
@section script-conversions Script type conversions
In most cases you can skip this section.
But if you define a script-accessible method with new types without having defined conversions,
you'll probably get mysterious linker errors that mention @c ToJSVal or @c FromJSVal.
First, work out where the conversion should be defined.
Basic data types (integers, STL containers, etc) go in scriptinterface/ScriptConversions.cpp.
Non-basic data types from the game engine typically go in simulation2/scripting/EngineScriptConversions.cpp.
(They could go in different files if that turns out to be cleaner - it doesn't matter where they're
defined as long as the linker finds them).
To convert from a C++ type @c T to a JS value, define:
@code
template<> jsval ScriptInterface::ToJSVal(JSContext* cx, T const& val)
{
...
}
@endcode
Use the standard SpiderMonkey JSAPI functions
to do the conversion (possibly calling @c ToJSVal recursively).
On error, you should return @c JSVAL_VOID (JS's @c undefined value) and probably report an error message somehow.
Be careful about JS garbage collection (don't let it collect the objects you're constructing before you return them).
To convert from a JS value to a C++ type @c T, define:
@code
template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval v, T& out)
{
...
}
@endcode
On error, return @c false (doesn't matter what you do with @c out).
On success, return @c true and put the value in @c out.
Still need to be careful about garbage collection (@c v is rooted, but it might have getters
that execute arbitrary code and return unrooted values when you access properties,
so don't let them be collected before you've finished using them).
@section defining-cpp-components Defining component types in C++
Now we want to implement the @c Example interface.
We need a name for the component type - if there's only ever going to be one implementation of the interface,
we might as well call it @c Example too.
If there's going to be more than one, they should have distinct names like @c ExampleStatic and @c ExampleMobile etc.
Create @b simulation2/components/CCmpExample.cpp:
\include CCmpExample.cpp
The only optional methods are @c HandleMessage and @c GetSchema - all others must be defined.
Update the file simulation2/TypeList.h and add:
@code
COMPONENT(Example)
@endcode
@subsection messages Message handling
First you need to register for all the message types you want to receive, in @c ClassInit:
@code
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(CID_Example, MT_Update);
...
}
@endcode
(@c CID_Example is derived from the name of the component type, @em not the name of the interface.)
You can also use SubscribeGloballyToMessageType, to intercept messages sent with PostMessage
that are targeted at a @em different entity. (Typically this is used by components that want
to hear about all MT_Destroy messages.)
Then you need to respond to the messages in @c HandleMessage:
@code
virtual void HandleMessage(const CSimContext& context, const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Update:
{
const CMessageUpdate& msgData = static_cast (msg);
Update(msgData.turnLength); // or whatever processing you want to do
break;
}
}
}
@endcode
The CMessage structures are defined in simulation2/MessageTypes.h. Be very careful that you're casting @c msg to the right type.
@subsection component-creation Component creation
Component type instances go through one of two lifecycles:
@code
CCmpExample();
Init(context, paramNode);
// any sequence of HandleMessage and Serialize and interface methods
Deinit(context);
~CCmpExample();
@endcode
@code
CCmpExample();
Deserialize(context, paramNode, deserialize);
// any sequence of HandleMessage and Serialize and interface methods
Deinit(context);
~CCmpExample();
@endcode
The order of Init/Deserialize/Deinit between entities is mostly undefined,
so they must not rely on other entities or components already existing; @em except that the SYSTEM_ENTITY is
created before anything else and therefore may be used, and that the components for a single entity will be
processed in the order determined by TypeList.h.
The same @c context object will be used in all these calls.
The context can also be accessed with IComponent::GetSimContext.
In a typical component:
- The constructor should do very little, other than perhaps initialising some member variables -
usually the default constructor is fine so there's no need to write one.
- @c Init should parse the @c paramNode (the data from the entity template) and store any needed data in member variables.
- @c Deserialize should often explicitly call @c Init first (to load the original template data), and then read any instance-specific data from the deserializer.
- @c Deinit should clean up any resources allocated by @c Init / @c Deserialize.
- The destructor should clean up any resources allocated by the constructor - usually there's no need to write one.
@subsection schema Component XML schemas
The @c paramNode passed to @c Init is constructed from XML entity template definition files.
Components should define a schema, which is used for several purposes:
- Documentation of the XML structure expected by the component.
- Automatic error checking that the XML matches the expectation, so the component doesn't have to do error checking itself.
- (Hopefully at some point in the future) Automatic generation of editing tool UI.
@c GetSchema must return a Relax NG fragment, which will be used to construct a single global schema file.
(You can run the game with the @c -dumpSchema command-line argument to see the schema).
The official tutorial describes most of the details
of the RNG language.
In simple cases, you would write something like:
@code
static std::string GetSchema()
{
return
""
""
""
""
"";
}
}
@endcode
i.e. a single string (C++ automatically concatenates the quoted lines) which defines a list of elements,
corresponding to an entity template XML file like:
@code
Barney235
@endcode
In the schema, each <element> has a name and some content.
The content will typically be one of:
- <empty/>
- <text/>
- <data type='boolean'/>
- <data type='decimal'/>
- <data type='nonNegativeInteger'/>
- <data type='positiveInteger'/>
- <ref name='nonNegativeDecimal'/>
- <ref name='positiveDecimal'/>
(The last two are slightly different since they're not standard data types.)
Elements can be wrapped in <optional>.
Groups of elements can be wrapped in <choice> to allow only one of them.
The content of an <element> can be further nested elements, but note that
elements may be reordered when loading an entity template:
if you specify a sequence of elements it should be wrapped in <interleave>,
so the schema checker will ignore reorderings of the sequence.
For early development of a new component, you can set the schema to <ref name='anything'/> to allow any content.
If you don't define @c GetSchema, then the default is <empty/> (i.e. there must be no elements).
@section allowing-js-interfaces Allowing interfaces to be implemented in JS
If we want to allow both C++ and JS implementations of @c ICmpExample,
we need to define a special component type that proxies all the C++ methods to the script.
Add the following to @b ICmpExample.cpp:
@code
#include "simulation2/scripting/ScriptComponent.h"
// ...
class CCmpExampleScripted : public ICmpExample
{
public:
DEFAULT_SCRIPT_WRAPPER(ExampleScripted)
virtual int DoWhatever(int x, int y)
{
return m_Script.Call ("DoWhatever", x, y);
}
};
REGISTER_COMPONENT_SCRIPT_WRAPPER(ExampleScripted)
@endcode
Then add to TypeList.h:
@code
COMPONENT(ExampleScripted)
@endcode
@c m_Script.Call takes the return type as a template argument,
then the name of the JS function to call and the list of parameters.
You could do extra conversion work before calling the script, if necessary.
You need to make sure the types are handled by @c ToJSVal and @c FromJSVal (as discussed before) as appropriate.
@section defining-js-components Defining component types in JS
Now we want a JS implementation of ICmpExample.
Think up a new name for this component, like @c ExampleTwo (but more imaginative).
Then write @b binaries/data/mods/public/simulation/components/ExampleTwo.js:
@code
function ExampleTwo() {}
ExampleTwo.prototype.Schema = "";
ExampleTwo.prototype.Init = function() {
...
};
ExampleTwo.prototype.Deinit = function() {
...
};
ExampleTwo.prototype.OnUpdate = function(msg) {
...
};
Engine.RegisterComponentType(IID_Example, "ExampleTwo", ExampleTwo);
@endcode
This uses JS's @em prototype system to create what is effectively a class, called @c ExampleTwo.
(If you wrote new ExampleTwo(), then JS would construct a new object which inherits from
@c ExampleTwo.prototype, and then would call the @c ExampleTwo function with @c this set to the new object.
"Inherit" here means that if you read a property (or method) of the object, which is not defined in the object,
then it will be read from the prototype instead.)
@c Engine.RegisterComponentType tells the engine to start using the JS class @c ExampleTwo,
exposed (in template files etc) with the name "ExampleTwo", and implementing the interface ID @c IID_Example
(i.e. the ICmpExample interface).
The @c Init and @c Deinit functions are optional. Unlike C++, there are no @c Serialize/Deserialize functions -
each JS component instance is automatically serialized and restored.
(This automatic serialization restricts what you can store as properties in the object - e.g. you cannot store function closures,
-because they're too hard to serialize. The details should be documented on some other page eventually.)
+because they're too hard to serialize. This will serialize Strings, numbers, bools, null, undefined, arrays of serializable
+values whose property names are purely numeric, objects whose properties are serializable values. Cyclic structures are allowed.)
Instead of @c ClassInit and @c HandleMessage, you simply add functions of the form OnMessageType.
(If you want the equivalent of SubscribeGloballyToMessageType, then use OnGlobalMessageType instead.)
When you call @c RegisterComponentType, it will find all such functions and automatically subscribe to the messages.
The @c msg parameter is usually a straightforward mapping of the relevant CMessage class onto a JS object
(e.g. @c OnUpdate can read @c msg.turnLength).
@section defining-js-interfaces Defining interface types in JS
If an interface is only ever used by JS components, and never implemented or called directly by C++ components,
then you don't need to do all of the work with defining ICmpExample.
Simply create a file @b binaries/data/mods/public/simulation/components/interfaces/Example.js:
@code
Engine.RegisterInterface("Example");
@endcode
You can then use @c IID_Example in JS components.
(There's no strict requirement to have a single .js file per interface definition,
it's just a convention that allows mods to easily extend the game with new interfaces.)
@section defining-cpp-message Defining a new message type in C++
Think of a name. We'll use @c Example again. (The name should typically be a present-tense verb, possibly
with a prefix to make its meaning clearer: "Update", "TurnStart", "RenderSubmit", etc).
Add to TypeList.h:
@code
MESSAGE(Example)
@endcode
Add to MessageTypes.h:
@code
class CMessageExample : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(Example)
CMessageExample(int x, int y) :
x(x), y(y)
{
}
int x;
int y;
};
@endcode
containing the data fields that are associated with the message. (In some cases there may be no fields.)
(If there are too many message types, MessageTypes.h could be split into multiple files with better organisation.
But for now everything is put in there.)
Now you have to add C++/JS conversions into MessageTypeConversions.cpp, so scripts can send and receive messages:
@code
jsval CMessageExample::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(x);
SET_MSG_PROPERTY(y);
return OBJECT_TO_JSVAL(obj);
}
CMessage* CMessageExample::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(int, x);
GET_MSG_PROPERTY(int, y);
return new CMessageExample(x, y);
}
@endcode
(You can use the JS API directly in here, but these macros simplify the common case of a single object
with a set of scalar fields.)
If you don't want to support scripts sending/receiving the message, you can implement stub functions instead:
@code
jsval CMessageExample::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const
{
return JSVAL_VOID;
}
CMessage* CMessageExample::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val))
{
return NULL;
}
@endcode
@section defining-js-message Defining a new message type in JS
If a message will only be sent and received by JS components, it can be defined purely in JS.
For example, add to the file @b interfaces/Example.js:
@code
// Message of the form { "foo": 1, "bar": "baz" }
// sent whenever the example component wants to demonstrate the message feature.
Engine.RegisterMessageType("Example");
@endcode
Note that the only specification of the structure of the message is in comments -
there is no need to tell the engine what properties it will have.
This message type can then be used from JS exactly like the @c CMessageExample defined in C++.
@section communication Component communication
@subsection message-passing Message passing
For one-to-many communication, you can send indirect messages to components.
From C++, use CComponentManager::PostMessage to send a message to a specific entity, and
CComponentManager::BroadcastMessage to send to all entities.
(In all cases, messages will only be received by components that subscribed to the corresponding message type).
@code
CMessageExample msg(10, 20);
context.GetComponentManager().PostMessage(ent, msg);
context.GetComponentManager().BroadcastMessage(msg);
@endcode
From JS, use @ref CComponentManager::Script_PostMessage "Engine.PostMessage" and
@ref CComponentManager::Script_BroadcastMessage "Engine.BroadcastMessage", using the
@c MT_* constants to identify the message type:
@code
Engine.PostMessage(ent, MT_Example, { x: 10, y: 20 });
Engine.BroadcastMessage(MT_Example, { x: 10, y: 20 });
@endcode
Messages will be received and processed synchronously, before the PostMessage/BroadcastMessage calls return.
@subsection query-interface Retrieving interfaces
You can also directly retrieve the component implementing a given interface for a given entity,
to call methods on it directly.
In C++, use CmpPtr (see its class documentation for details):
@code
#include "simulation2/components/ICmpPosition.h"
...
CmpPtr cmpPosition(context, ent);
if (cmpPosition.null())
// do something to avoid dereferencing null pointers
cmpPosition->MoveTo(x, y);
@endcode
In JS, use @ref CComponentManager::Script_QueryInterface "Engine.QueryInterface":
@code
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.MoveTo(x, y);
@endcode
(The use of @c cmpPosition in JS will throw an exception if it's null, so there's no need
for explicit checks unless you expect the component may legitimately not exist and you want
to handle it gracefully.)
@section testing Testing components
Tests are critical for ensuring and maintaining code quality, so all non-trivial components should
have test cases. The first part is testing each component in isolation, to check the following aspects:
- Initialising the component state from template data.
- Responding to method calls to modify and retrieve state.
- Responding to broadcast/posted messages.
- Serializing and deserializing, for saved games and networking.
To focus on these, the communication and interaction with other components is explicitly not tested here
(though it should be tested elsewhere).
The code for the tested component is loaded, but all other components are replaced with mock objects
that implement the expected interfaces but with dummy implementations (ignoring calls, returning constants, etc).
The details differ depending on what language the component is written in:
@subsection testing-cpp Testing C++ components
Create the file @b simulation2/components/tests/test_Example.h, and copy it from something like test_CommandQueue.h.
In particular, you need the @c setUp and @c tearDown functions to initialise CXeromyces, and you should use
ComponentTestHelper to set up the test environment and construct the component for you.
Then just use the component, and use CxxTest's @c TS_* macros to check things, and use
ComponentTestHelper::Roundtrip to test serialization roundtripping.
Define mock component objects similarly to MockTerrain. Put it in ComponentTest.h if it's usable by many
component tests, or in the test_*.h file if it's specific to one test.
Instantiate a mock object on the stack, and use ComponentTestHelper::AddMock to make it accessible
by QueryInterface.
@subsection testing-js Testing JS components
Create the file @b binaries/data/mods/public/simulation/components/tests/test_ExampleTwo.js, and write
@code
Engine.LoadComponentScript("ExampleTwo.js");
var cmp = ConstructComponent(1, "ExampleTwo");
@endcode
where @c Example.js is the component script to test, @c 1 is the entity ID, @c "ExampleTwo" is the component name.
Then call methods on @c cmp to test it, using the @c TS_* functions defined in
@b binaries/data/tests/test_setup.js for common assertions.
Create mock objects like
@code
AddMock(1, IID_Position, {
GetPosition: function() {
return {x:1, y:2, z:3};
},
});
@endcode
giving the entity ID, interface ID, and an object that emulates as much of the interface as is needed
for the test.
*/
Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 11886)
@@ -1,493 +1,698 @@
function Attack() {}
var bonusesSchema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
var preferredClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
var restrictedClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.Schema =
"Controls the attack abilities and strengths of the unit." +
"" +
"" +
"10.0" +
"0.0" +
"5.0" +
"4.0" +
"1000" +
"" +
"" +
"pers" +
"Infantry" +
"1.5" +
"" +
"" +
"Cavalry Melee" +
"1.5" +
"" +
"" +
"Champion" +
"Cavalry Infantry" +
"" +
"" +
"0.0" +
"10.0" +
"0.0" +
"44.0" +
"20.0" +
"800" +
"1600" +
"50.0" +
+ "2.5" +
"" +
"" +
"Cavalry" +
"2" +
"" +
"" +
"Champion" +
+ "" +
+ "Circular" +
+ "20" +
+ "false" +
+ "0.0" +
+ "10.0" +
+ "0.0" +
+ "" +
"" +
"" +
"10.0" +
"0.0" +
"50.0" +
"24.0" +
"20.0" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
bonusesSchema +
preferredClassesSchema +
restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
+ "" +
bonusesSchema +
preferredClassesSchema +
restrictedClassesSchema +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ bonusesSchema +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" + // TODO: how do these work?
"" +
bonusesSchema +
preferredClassesSchema +
restrictedClassesSchema +
"" +
"" +
"";
Attack.prototype.Init = function()
{
};
Attack.prototype.Serialize = null; // we have no dynamic state to save
Attack.prototype.GetAttackTypes = function()
{
var ret = [];
if (this.template.Charge) ret.push("Charge");
if (this.template.Melee) ret.push("Melee");
if (this.template.Ranged) ret.push("Ranged");
return ret;
};
Attack.prototype.GetPreferredClasses = function(type)
{
if (this.template[type] && this.template[type].PreferredClasses)
{
return this.template[type].PreferredClasses._string.split(/\s+/);
}
return [];
};
Attack.prototype.GetRestrictedClasses = function(type)
{
if (this.template[type] && this.template[type].RestrictedClasses)
{
return this.template[type].RestrictedClasses._string.split(/\s+/);
}
return [];
};
Attack.prototype.CanAttack = function(target)
{
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
const targetClasses = cmpIdentity.GetClassesList();
for each (var type in this.GetAttackTypes())
{
var canAttack = true;
var restrictedClasses = this.GetRestrictedClasses(type);
for each (var targetClass in targetClasses)
{
if (restrictedClasses.indexOf(targetClass) != -1)
{
canAttack = false;
break;
}
}
if (canAttack)
{
return true;
}
}
return false;
};
/**
* Returns null if we have no preference or the lowest index of a preferred class.
*/
Attack.prototype.GetPreference = function(target)
{
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
const targetClasses = cmpIdentity.GetClassesList();
var minPref = null;
for each (var type in this.GetAttackTypes())
{
for each (var targetClass in targetClasses)
{
var pref = this.GetPreferredClasses(type).indexOf(targetClass);
if (pref != -1 && (minPref === null || minPref > pref))
{
minPref = pref;
}
}
}
return minPref;
};
/**
* Return the type of the best attack.
* TODO: this should probably depend on range, target, etc,
* so we can automatically switch between ranged and melee
*/
Attack.prototype.GetBestAttack = function()
{
return this.GetAttackTypes().pop();
};
Attack.prototype.GetBestAttackAgainst = function(target)
{
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
const targetClasses = cmpIdentity.GetClassesList();
const isTargetClass = function (value, i, a) { return targetClasses.indexOf(value) != -1; };
const types = this.GetAttackTypes();
const attack = this;
const isAllowed = function (value, i, a) { return !attack.GetRestrictedClasses(value).some(isTargetClass); }
const isPreferred = function (value, i, a) { return attack.GetPreferredClasses(value).some(isTargetClass); }
const byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }
return types.filter(isAllowed).sort(byPreference).pop();
};
Attack.prototype.CompareEntitiesByPreference = function(a, b)
{
var aPreference = this.GetPreference(a);
var bPreference = this.GetPreference(b);
if (aPreference === null && bPreference === null) return 0;
if (aPreference === null) return 1;
if (bPreference === null) return -1;
return aPreference - bPreference;
};
Attack.prototype.GetTimers = function(type)
{
var prepare = +(this.template[type].PrepareTime || 0);
var repeat = +(this.template[type].RepeatTime || 1000);
var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (cmpTechMan)
{
prepare = cmpTechMan.ApplyModifications("Attack/" + type + "/PrepareTime", prepare, this.entity);
repeat = cmpTechMan.ApplyModifications("Attack/" + type + "/RepeatTime", repeat, this.entity);
}
return { "prepare": prepare, "repeat": repeat, "recharge": repeat - prepare };
};
Attack.prototype.GetAttackStrengths = function(type)
{
// Work out the attack values with technology effects
var self = this;
+ var template = this.template[type];
+ var splash = "";
+ if (!template)
+ {
+ template = this.template[type.split(".")[0]].Splash;
+ splash = "/Splash";
+ }
+
var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager);
var applyTechs = function(damageType)
{
var strength = +(self.template[type][damageType] || 0);
if (cmpTechMan)
{
// All causes caching problems so disable it for now.
- //var allComponent = cmpTechMan.ApplyModifications("Attack/" + type + "/All", strength, self.entity) - self.template[type][damageType];
- strength = cmpTechMan.ApplyModifications("Attack/" + type + "/" + damageType, strength, self.entity);
+ //var allComponent = cmpTechMan.ApplyModifications("Attack/" + type + splash + "/All", strength, self.entity) - self.template[type][damageType];
+ strength = cmpTechMan.ApplyModifications("Attack/" + type + splash + "/" + damageType, strength, self.entity);
}
return strength;
};
return {
hack: applyTechs("Hack"),
pierce: applyTechs("Pierce"),
crush: applyTechs("Crush")
};
};
Attack.prototype.GetRange = function(type)
{
var max = +this.template[type].MaxRange;
var min = +(this.template[type].MinRange || 0);
var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (cmpTechMan)
{
max = cmpTechMan.ApplyModifications("Attack/" + type + "/MaxRange", max, this.entity);
min = cmpTechMan.ApplyModifications("Attack/" + type + "/MinRange", min, this.entity);
}
return { "max": max, "min": min };
};
// Calculate the attack damage multiplier against a target
Attack.prototype.GetAttackBonus = function(type, target)
{
var attackBonus = 1;
- if (this.template[type].Bonuses)
+ var template = this.template[type];
+ if (!template)
+ template = this.template[type.split(".")[0]].Splash;
+
+ if (template.Bonuses)
{
var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return 1;
// Multiply the bonuses for all matching classes
- for (var key in this.template[type].Bonuses)
+ for (var key in template.Bonuses)
{
- var bonus = this.template[type].Bonuses[key];
+ var bonus = template.Bonuses[key];
var hasClasses = true;
if (bonus.Classes){
var classes = bonus.Classes.split(/\s+/);
for (var key in classes)
hasClasses = hasClasses && cmpIdentity.HasClass(classes[key]);
}
if (hasClasses && (!bonus.Civ || bonus.Civ === cmpIdentity.GetCiv()))
attackBonus *= bonus.Multiplier;
}
}
return attackBonus;
};
+// Returns a 2d random distribution scaled for a spread of scale 1.
+// The current implementation is a 2d gaussian with sigma = 1
+Attack.prototype.GetNormalDistribution = function(){
+
+ // Use the Box-Muller transform to get a gaussian distribution
+ var a = Math.random();
+ var b = Math.random();
+
+ var c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b);
+ var d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b);
+
+ return [c, d];
+};
+
/**
* 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(type, target)
{
// If this is a ranged attack, then launch a projectile
if (type == "Ranged")
{
- // 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.)
+ // In the future this could be extended:
// * 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[type].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 spread = this.template.Ranged.Spread;
+
+ //horizSpeed /= 2; gravity /= 2; // slow it down for testing
+
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var selfPosition = cmpPosition.GetPosition();
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
var targetPosition = cmpTargetPosition.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, {"type": type, "target": target});
- }
+
+ var relativePosition = {"x": targetPosition.x - selfPosition.x, "z": targetPosition.z - selfPosition.z}
+ var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
+
+ var targetVelocity = {"x": (targetPosition.x - previousTargetPosition.x) / this.turnLength, "z": (targetPosition.z - previousTargetPosition.z) / this.turnLength}
+ // the component of the targets velocity radially away from the archer
+ var radialSpeed = this.VectorDot(relativePosition, targetVelocity) / this.VectorLength(relativePosition);
+
+ var horizDistance = this.VectorDistance(targetPosition, selfPosition);
+
+ // This is an approximation of the time ot the target, it assumes that the target has a constant radial
+ // velocity, but since units move in straight lines this is not true. The exact value would be more
+ // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was
+ // about 5% of the units radius out in the worst case)
+ var timeToTarget = horizDistance / (horizSpeed - radialSpeed);
+
+ // Predict where the unit is when the missile lands.
+ var predictedPosition = {"x": targetPosition.x + targetVelocity.x * timeToTarget,
+ "z": targetPosition.z + targetVelocity.z * timeToTarget};
+
+ // Compute the real target point (based on spread and target speed)
+ var randNorm = this.GetNormalDistribution();
+ var offsetX = randNorm[0] * spread * (1 + this.VectorLength(targetVelocity) / 20);
+ var offsetZ = randNorm[1] * spread * (1 + this.VectorLength(targetVelocity) / 20);
+ var realTargetPosition = { "x": predictedPosition.x + offsetX, "y": targetPosition.y, "z": predictedPosition.z + offsetZ };
+
+ // Calculate when the missile will hit the target position
+ var realHorizDistance = this.VectorDistance(realTargetPosition, selfPosition);
+ var timeToTarget = realHorizDistance / horizSpeed;
+
+ var missileDirection = {"x": (realTargetPosition.x - selfPosition.x) / realHorizDistance, "z": (realTargetPosition.z - selfPosition.z) / realHorizDistance};
+
+ // Make the arrow appear to land slightly behind the target so that arrows landing next to a guys foot don't count but arrows that go through the torso do
+ var graphicalPosition = {"x": realTargetPosition.x + 2*missileDirection.x, "y": realTargetPosition.y + 2*missileDirection.y};
// 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);
+ var id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity);
+
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id});
}
else
{
// Melee attack - hurt the target immediately
this.CauseDamage({"type": type, "target": target});
}
// TODO: charge attacks (need to design how they work)
};
/**
* Called when some units kills something (another unit, building, animal etc)
*/
Attack.prototype.TargetKilled = function(killerEntity, targetEntity)
{
var cmpKillerPlayerStatisticsTracker = QueryOwnerInterface(killerEntity, IID_StatisticsTracker);
if (cmpKillerPlayerStatisticsTracker) cmpKillerPlayerStatisticsTracker.KilledEntity(targetEntity);
var cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(targetEntity, IID_StatisticsTracker);
if (cmpTargetPlayerStatisticsTracker) cmpTargetPlayerStatisticsTracker.LostEntity(targetEntity);
// if unit can collect loot, lets try to collect it
var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter);
if (cmpLooter)
{
cmpLooter.Collect(targetEntity);
}
};
+Attack.prototype.InterpolatedLocation = function(ent, lateness)
+{
+ var targetPositionCmp = Engine.QueryInterface(ent, IID_Position);
+ if (!targetPositionCmp) // TODO: handle dead target properly
+ return undefined;
+ var curPos = targetPositionCmp.GetPosition();
+ var prevPos = targetPositionCmp.GetPreviousPosition();
+ lateness /= 1000;
+ return {"x": (curPos.x * (this.turnLength - lateness) + prevPos.x * lateness) / this.turnLength,
+ "z": (curPos.z * (this.turnLength - lateness) + prevPos.z * lateness) / this.turnLength};
+};
+
+Attack.prototype.VectorDistance = function(p1, p2)
+{
+ return Math.sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.z - p2.z)*(p1.z - p2.z));
+};
+
+Attack.prototype.VectorDot = function(p1, p2)
+{
+ return (p1.x * p2.x + p1.z * p2.z);
+};
+
+Attack.prototype.VectorCross = function(p1, p2)
+{
+ return (p1.x * p2.z - p1.z * p2.x);
+};
+
+Attack.prototype.VectorLength = function(p)
+{
+ return Math.sqrt(p.x*p.x + p.z*p.z);
+};
+
+// Tests whether it point is inside of ent's footprint
+Attack.prototype.testCollision = function(ent, point, lateness)
+{
+ var targetPosition = this.InterpolatedLocation(ent, lateness);
+ var targetShape = Engine.QueryInterface(ent, IID_Footprint).GetShape();
+
+ if (!targetShape || !targetPosition)
+ return false;
+
+ if (targetShape.type === 'circle')
+ {
+ return (this.VectorDistance(point, targetPosition) < targetShape.radius);
+ }
+ else
+ {
+ var targetRotation = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
+
+ var dx = point.x - targetPosition.x;
+ var dz = point.z - targetPosition.z;
+
+ var dxr = Math.cos(targetRotation) * dx - Math.sin(targetRotation) * dz;
+ var dzr = Math.sin(targetRotation) * dx + Math.cos(targetRotation) * dz;
+
+ return (-targetShape.width/2 <= dxr && dxr < targetShape.width/2 && -targetShape.depth/2 <= dzr && dzr < targetShape.depth/2);
+ }
+};
+
+Attack.prototype.MissileHit = function(data, lateness)
+{
+
+ var targetPosition = this.InterpolatedLocation(data.target, lateness);
+ if (!targetPosition)
+ return;
+
+ if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target
+ {
+ var friendlyFire = this.template.Ranged.Splash.FriendlyFire;
+ var splashRadius = this.template.Ranged.Splash.Range;
+ var splashShape = this.template.Ranged.Splash.Shape;
+
+ var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2 + splashRadius, friendlyFire);
+ ents.push(data.target); // Add the original unit to the list of splash damage targets
+
+ for (var i = 0; i < ents.length; i++)
+ {
+ var entityPosition = this.InterpolatedLocation(ents[i], lateness);
+ var radius = this.VectorDistance(data.position, entityPosition);
+
+ if (radius < splashRadius)
+ {
+ var multiplier = 1;
+ if (splashShape == "Circular") // quadratic falloff
+ {
+ multiplier *= 1 - ((radius * radius) / (splashRadius * splashRadius));
+ }
+ else if (splashShape == "Linear")
+ {
+ // position of entity relative to where the missile hit
+ var relPos = {"x": entityPosition.x - data.position.x, "z": entityPosition.z - data.position.z};
+
+ var splashWidth = splashRadius / 5;
+ var parallelDist = this.VectorDot(relPos, data.direction);
+ var perpDist = Math.abs(this.VectorCross(relPos, data.direction));
+
+ // Check that the unit is within the distance splashWidth of the line starting at the missile's
+ // landing point which extends in the direction of the missile for length splashRadius.
+ if (parallelDist > -splashWidth && perpDist < splashWidth)
+ {
+ // Use a quadratic falloff in both directions
+ multiplier = (splashRadius*splashRadius - parallelDist*parallelDist) / (splashRadius*splashRadius)
+ * (splashWidth*splashWidth - perpDist*perpDist) / (splashWidth*splashWidth);
+ }
+ else
+ {
+ multiplier = 0;
+ }
+ }
+ var newData = {"type": data.type + ".Splash", "target": ents[i], "damageMultiplier": multiplier};
+ this.CauseDamage(newData);
+ }
+ }
+ }
+
+ if (this.testCollision(data.target, data.position, lateness))
+ {
+ // Hit the primary target
+ this.CauseDamage(data);
+
+ // Remove the projectile
+ var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
+ cmpProjectileManager.RemoveProjectile(data.projectileId);
+ }
+ else
+ {
+ // If we didn't hit the main target look for nearby units
+ var ents = this.GetNearbyEntities(data.target, this.VectorDistance(data.position, targetPosition) * 2);
+
+ for (var i = 0; i < ents.length; i++)
+ {
+ if (this.testCollision(ents[i], data.position, lateness))
+ {
+ var newData = {"type": data.type, "target": ents[i]};
+ this.CauseDamage(newData);
+
+ // Remove the projectile
+ var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
+ cmpProjectileManager.RemoveProjectile(data.projectileId);
+ }
+ }
+ }
+};
+
+Attack.prototype.GetNearbyEntities = function(startEnt, range, friendlyFire)
+{
+ var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+ var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ var owner = cmpOwnership.GetOwner();
+ var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player);
+ var numPlayers = cmpPlayerManager.GetNumPlayers();
+ var players = [];
+
+ for (var i = 1; i < numPlayers; ++i)
+ {
+ // Only target enemies unless friendly fire is on
+ if (cmpPlayer.IsEnemy(i) || friendlyFire)
+ players.push(i);
+ }
+
+ var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ return rangeManager.ExecuteQuery(startEnt, 0, range, players, IID_DamageReceiver);
+}
+
/**
* Inflict damage on the target
*/
Attack.prototype.CauseDamage = function(data)
{
var strengths = this.GetAttackStrengths(data.type);
- var attackBonus = this.GetAttackBonus(data.type, data.target);
+ var damageMultiplier = this.GetAttackBonus(data.type, data.target);
+ if (data.damageMultiplier !== undefined)
+ damageMultiplier *= data.damageMultiplier;
var cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
if (!cmpDamageReceiver)
return;
- var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * attackBonus, strengths.pierce * attackBonus, strengths.crush * attackBonus);
+ var targetState = cmpDamageReceiver.TakeDamage(strengths.hack * damageMultiplier, strengths.pierce * damageMultiplier, strengths.crush * damageMultiplier);
// if target killed pick up loot and credit experience
if (targetState.killed == true)
{
this.TargetKilled(this.entity, data.target);
}
Engine.PostMessage(data.target, MT_Attacked,
{ "attacker": this.entity, "target": data.target, "type": data.type });
PlaySound("attack_impact", this.entity);
};
+Attack.prototype.OnUpdate = function(msg)
+{
+ this.turnLength = msg.turnLength;
+}
+
Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_outpost.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_outpost.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_outpost.xml (revision 11886)
@@ -1,86 +1,87 @@
5.05.05.00.025.00.055.013.075.012002000
+ 1.51own neutralHouse4080015.010.1Support Infantry02800OutpostBuild in neutral and friendly territories to scout areas of the map. Slowly loses health while in neutral territory.Village Defensive -ConquestCriticalstructures/outpost.png1000800interface/complete/building/complete_tower.xmlattack/destruction/building_collapse_large.xml6.00.618.0180props/special/palisade_rocks_outpost.xmlstructures/fndn_2x2.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml (revision 11886)
@@ -1,61 +1,62 @@
6.06.05.00.030.00.05810.075.012002000
-
-
- Infantry Sword
- 2.0
-
-
- Cavalry Spear
- 1.5
-
-
+ 1.5
+
+
+ Infantry Sword
+ 2.0
+
+
+ Cavalry Spear
+ 1.5
+
+ OrganicStoneWall12507500120Ranged BowChampion ArcherChampion Archer.
Counters: 2x vs. Swordsmen, 1.5x vs. Cavalry Spearmen. Countered by: Cavalry Swordsmen, Cavalry Skirmishers.attack/weapon/arrowfly.xml9.018.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml (revision 11886)
@@ -1,69 +1,70 @@
6.06.05.00.030.00.04812.050.012002000
-
-
- Infantry Spear
- 1.5
-
-
- Cavalry Bow
- 1.5
-
-
- Elephant
- 1.5
-
-
- Chariot
- 1.5
-
-
+ 1.5
+
+
+ Infantry Spear
+ 1.5
+
+
+ Cavalry Bow
+ 1.5
+
+
+ Elephant
+ 1.5
+
+
+ Chariot
+ 1.5
+
+ OrganicStoneWall12508000120Ranged JavelinChampion SkirmisherChampion Skirmisher.
Counters: 1.5x vs. Spearmen, Cavalry Archers, Elephants, and Chariots. Countered by: Swordsmen, Cavalry Spearmen.attack/weapon/arrowfly.xml9.018.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml (revision 11886)
@@ -1,89 +1,90 @@
15.040.015.00.025.00.080.016.075.012002000
+ 1.511DefenseTower12010010015.050.1Support Infantry021200TowerShoots arrows. Garrison to provide extra defense.Town Defensive Tower GarrisonTower -ConquestCriticalstructures/defense_tower.pngphase_town100010100interface/complete/building/complete_tower.xmlattack/weapon/arrowfly.xmlattack/destruction/building_collapse_large.xml6.00.621.0false326553680structures/fndn_2x2.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml (revision 11886)
@@ -1,60 +1,61 @@
6.06.05.00.030.00.05616.050.012002000
-
-
- Infantry Bow
- 2.0
-
-
- Cavalry Sword
- 1.5
-
-
+ 1.5
+
+
+ Infantry Bow
+ 2.0
+
+
+ Cavalry Sword
+ 1.5
+
+ StoneWall10080Ranged JavelinChampion Cavalry Skirmisher.Champion Cavalry Skirmisher.
Counters: 2x vs. Archers, 1.5x vs. Cavalry Swordsmen. Countered by: Spearmen, Elephants.actor/fauna/death/death_horse.xmlattack/weapon/arrowfly.xml11.528.751000.010.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml (revision 11886)
@@ -1,30 +1,31 @@
0.01.00.01.01.028.012002000
+ 1.5OrganicStoneWall900.2RangedRangedattack/weapon/arrowfly.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml (revision 11886)
@@ -1,68 +1,77 @@
4.02.00.0
- 50.0
- 50.0
+ 20.0
+ 20.0608.0
+ 6.060.050005000
-
-
- Organic
- 2.0
-
-
+
+
+ Organic
+ 2.0
+
+
+
+ Linear
+ 12
+ false
+ 0.0
+ 30.0
+ 30.0
+ 2010010022.0200Bolt ShooterBonused vs. Infantry and Cavalry units.200010010circle/256x256.pngcircle/256x256_mask.png6.512.070
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml (revision 11886)
@@ -1,82 +1,83 @@
7.06.00.025.00.065.02.075.012002000
+ 1.51155050030020.0200.1Support Infantry12800Siege TowerA mobile missile platform.circle/256x256.pngcircle/256x256_mask.pngattack/siege/ram_move.xmlattack/siege/ram_attack.xmlattack/siege/ram_move.xml4.00.512.06.010.080
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wallset.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wallset.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wallset.xml (revision 11886)
@@ -1,19 +1,18 @@
structures/wall.pngTown WallCity WallWall off your town for a stout defense.
- structures/wall.png
- phase_town
+ phase_town0.850.05
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml (revision 11886)
@@ -1,89 +1,90 @@
10.040.020.00.025.00.080.012.075.012002000
+ 1.530.5own neutral enemyFortress5160400012.040Support Infantry Cavalry Siege13600falseromeEntrenched Army CampCastra Vellumstructures/fortress.pngBuild anywhere on the map, even in enemy territory. Construct siege weapons and train citizen-soldiers. Heal garrisoned units slowly.Sometimes it was a temporary camp built facing the route by which the army is to march, other times a defensive or offensive (for sieges) structure. Within this gate the tents of the first centuries or cohorts are pitched, and the dragons (ensigns of cohorts) and other ensigns planted. The Decumane gate is directly opposite to the Praetorian in the rear of the camp, and through this the soldiers are conducted to the place appointed for punishment or execution.interface/complete/building/complete_broch.xmlattack/destruction/building_collapse_large.xml5
units/rome_infantry_swordsman_b
units/rome_infantry_spearman_a
units/rome_infantry_javelinist_b
units/rome_cavalry_spearman_b
units/rome_mechanical_siege_ballista
units/rome_mechanical_siege_scorpio
units/rome_mechanical_siege_ram
60structures/fndn_8x8.xmlstructures/romans/camp.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml (revision 11886)
@@ -1,78 +1,79 @@
8.08.08.00.040.00.060.020.075.012002000
-
-
- Infantry Spear
- 2.0
-
-
- Infantry Sword
- 1.5
-
-
+ 1.5
+
+
+ Infantry Spear
+ 2.0
+
+
+ Infantry Sword
+ 1.5
+
+ 451001001003.05000.5Hero Bow -JavelinHero Cavalry ArcherHero Aura: n/a.
Ranged attack 2x vs. spearmen. Ranged attack 1.5x vs. Swordsmen.450102000herostar/256x256.pngstar/256x256_mask.png11.025.01000.016.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 11886)
@@ -1,108 +1,109 @@
20.04010.00.025.00.060.010.075.012002000
+ 1.510.5own neutralCivilCentreCivilCentre1802030005005005008.0200.1Support Infantry Cavalry123000Civic CentreBuild to acquire large tracts of territory. Train citizens. Garrison: 20.
Village
Defensive
CivCentre
structures/civic_centre.pngphase_town2000505050
units/{civ}_support_female_citizen
phase_town
phase_city
food wood stone metalinterface/complete/building/complete_civ_center.xmlattack/weapon/arrowfly.xmlattack/destruction/building_collapse_large.xmltrue1406553690structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_mechanical_siege_fireraiser.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_mechanical_siege_fireraiser.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_mechanical_siege_fireraiser.xml (revision 11886)
@@ -1,34 +1,35 @@
50.00.050.0128.010.020002000
+ 1.54.5thebFire RaiserPyro-somethingunits/hele_mechanical_siege_lithobolos.png60units/thebans/siege_fireraiser.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 11886)
@@ -1,69 +1,67 @@
10.040.020.0Market1503008.01500MarketCreate trade units to trade between other markets. Barter resources.Town Market BarterMarketstructures/market.pngphase_town10025252525
-
-
- armor_trade_convoys
-
- interface/complete/building/complete_market.xmlattack/destruction/building_collapse_large.xmlfalse4065536
+
+ armor_trade_convoys
+
units/{civ}_support_trader
32structures/fndn_5x5.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/other/plane.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/other/plane.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/other/plane.xml (revision 11886)
@@ -1,42 +1,43 @@
0.0100.025.0482460.002000
+ 1.5heleP-51 MustangThis may be anachronistic.A World War 2 American fighter plane.units/global_mustang.png1.040.015.01.02.050.05.0100units/global/plane.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml (revision 11886)
@@ -1,76 +1,85 @@
4.010.0
- 50.0
+ 40.00.0
- 50.0
+ 40.06812.030.050005000
+ 6.0Structure2.0
+
+ Circular
+ 12
+ true
+ 12.0
+ 0.0
+ 12.0
+ 302001004.5300Siege CatapultBonused vs. Structures and Massed Infantry.300020100circle/256x256.pngcircle/256x256_mask.pngattack/siege/ballist_attack.xml4.00.55.510.080
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_ranged.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_ranged.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_ranged.xml (revision 11886)
@@ -1,50 +1,51 @@
5.010.010.00.050.00.056.025.012002000
+ 1.5601005004000.2HeroHero Infantry Ranged200105000hero8.521.25
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml (revision 11886)
@@ -1,59 +1,60 @@
5.05.05.00.050.0100.085.020.040.020004000
+ 1.55452502008.0500Support Infantry Cavalry Siege1102000Heavy WarshipHeavy Warship.Warship6.00.56.014.5110
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml (revision 11885)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml (revision 11886)
@@ -1,68 +1,69 @@
3.03.03.00.040.00.080.010.060.010002000
+ 1.5313302001508.0300Support Infantry Cavalry Siege1101400Medium WarshipMedium Warship.Warshipattack/impact/arrow_metal.xml6.00.56.014.520.0