1
0
forked from 0ad/0ad

Implements entity terrain anchoring as a visual effect, based on patch by sanderd17, fixes #1988.

Sets cavalry and quadrupedal animals to 'pitch' anchoring. Cleans up
mine templates.

This was SVN commit r13565.
This commit is contained in:
historic_bruno 2013-07-16 03:46:30 +00:00
parent 9a0ab14cf3
commit f302faf8e4
46 changed files with 249 additions and 74 deletions

View File

@ -18,6 +18,9 @@
<SpecificName>Bear</SpecificName>
<Icon>gaia/fauna_bear.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Sound>
<SoundGroups>
<select>actor/fauna/animal/lion_select.xml</select>

View File

@ -18,6 +18,9 @@
<SpecificName>Boar</SpecificName>
<Icon>gaia/fauna_boar.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>150</Amount>
<Type>food.meat</Type>

View File

@ -12,6 +12,9 @@
<Obstruction>
<Unit radius="1.5"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Sound>
<SoundGroups>
<select>actor/fauna/animal/camel.xml</select>

View File

@ -5,6 +5,9 @@
<SpecificName>Deer</SpecificName>
<Icon>gaia/fauna_deer.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<UnitMotion>
<WalkSpeed>2.0</WalkSpeed>
<Run>

View File

@ -14,6 +14,9 @@
<SpecificName>Elephant</SpecificName>
<Icon>gaia/fauna_elephant.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Sound>
<SoundGroups>
<select>actor/fauna/animal/elephant_select.xml</select>

View File

@ -53,6 +53,9 @@
<Obstruction>
<Unit radius="3.5"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>800</Amount>
<Type>food.meat</Type>

View File

@ -18,6 +18,9 @@
<Obstruction>
<Unit radius="1.5"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>100</Amount>
<Type>food.meat</Type>

View File

@ -53,6 +53,9 @@
<Obstruction>
<Unit radius="2.7"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>650</Amount>
<Type>food.meat</Type>

View File

@ -5,6 +5,9 @@
<SpecificName>Gazelle</SpecificName>
<Icon>gaia/fauna_gazelle.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<UnitMotion>
<Run>
<Range>600.0</Range>

View File

@ -16,6 +16,9 @@
<Obstruction>
<Unit radius="2.0"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>350</Amount>
<Type>food.meat</Type>

View File

@ -12,6 +12,9 @@
<Obstruction>
<Unit radius="1.5"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>150</Amount>
<Type>food.meat</Type>

View File

@ -11,6 +11,9 @@
<SpecificName>Goat</SpecificName>
<Icon>gaia/fauna_goat.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>120</Amount>
<Type>food.meat</Type>

View File

@ -12,6 +12,9 @@
<Obstruction>
<Unit radius="1.5"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>200</Amount>
</ResourceSupply>

View File

@ -18,6 +18,9 @@
<SpecificName>Lion</SpecificName>
<Icon>gaia/fauna_lion.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Sound>
<SoundGroups>
<select>actor/fauna/animal/lion_select.xml</select>

View File

@ -18,6 +18,9 @@
<SpecificName>Lion</SpecificName>
<Icon>gaia/fauna_lion.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Sound>
<SoundGroups>
<select>actor/fauna/animal/lion_select.xml</select>

View File

@ -9,6 +9,9 @@
<SpecificName>Muskox</SpecificName>
<Icon>gaia/fauna_muskox.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>200</Amount>
<Type>food.meat</Type>

View File

@ -5,6 +5,9 @@
<SpecificName>Pig</SpecificName>
<Icon>gaia/fauna_pig.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<StatusBars>
<HeightOffset>5.0</HeightOffset>
</StatusBars>

View File

@ -9,6 +9,9 @@
<SpecificName>Rabbit</SpecificName>
<Icon>gaia/fauna_rabbit.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>50</Amount>
<Type>food.meat</Type>

View File

@ -12,6 +12,9 @@
<Classes datatype="tokens">Domestic</Classes>
<Icon>gaia/fauna_sheep.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Sound>
<SoundGroups>
<select>actor/fauna/animal/sheep.xml</select>

View File

@ -18,6 +18,9 @@
<SpecificName>Tiger</SpecificName>
<Icon>gaia/fauna_tiger.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Sound>
<SoundGroups>
<select>actor/fauna/animal/lion_select.xml</select>

View File

@ -23,6 +23,9 @@
<SpecificName>Walrus</SpecificName>
<Icon>gaia/fauna_walrus.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>300</Amount>
<Type>food.meat</Type>

View File

@ -10,6 +10,9 @@
<History>The wildebeest (plural wildebeest, wildebeests or wildebai), also called the gnu, is an antelope of the genus Connochaetes. It is a hooved (ungulate) mammal. Wildebeest are well known for their annual migration to new pastures in which vast numbers of wildebeest can be seen crossing rivers, such as the Mara River and dying in large numbers as they attempt to reach the other side. Many of them are eaten by crocodiles while others simply drown. Herds of wildebeest possesses what is known as "swarm intelligence", whereby the animals systematically explore and overcome obstacles as one when, for instance, crossing a river or defending against predator attacks.</History>
<Icon>gaia/fauna_wildebeest.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>150</Amount>
<Type>food.meat</Type>

View File

@ -18,6 +18,9 @@
<SpecificName>Wolf</SpecificName>
<Icon>gaia/fauna_wolf.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<VisualActor>
<Actor>fauna/wolf.xml</Actor>
</VisualActor>

View File

@ -18,6 +18,9 @@
<SpecificName>Snow Wolf</SpecificName>
<Icon>gaia/fauna_wolf_snow.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<VisualActor>
<Actor>fauna/wolf_snow.xml</Actor>
</VisualActor>

View File

@ -9,6 +9,9 @@
<SpecificName>Zebra</SpecificName>
<Icon>gaia/fauna_zebra.png</Icon>
</Identity>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceSupply>
<Amount>150</Amount>
<Type>food.meat</Type>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_mineral">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/metalmine_alpine.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_mineral">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/metalmine_desert_small.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_mineral">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/metalmine_granite_greek.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_mineral">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/metalmine_mediterranean.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_mineral">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/metalmine_granite_temperate.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_mineral">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/metalmine_tropic.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_rock">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/stonemine_alpine_a.xml</Actor>
</VisualActor>

View File

@ -7,9 +7,6 @@
<Identity>
<Icon>gaia/geology_stone_2.png</Icon>
</Identity>
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/stonemine_desert_small.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_rock">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/stonemine_granite_greek.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_rock">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/stonemine_mediterranean.xml</Actor>
</VisualActor>

View File

@ -7,9 +7,6 @@
<Identity>
<Icon>gaia/geology_stone_2.png</Icon>
</Identity>
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/stonemine_savanna_small.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_rock">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/stonemine_granite.xml</Actor>
</VisualActor>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_gaia_geo_rock">
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<VisualActor>
<Actor>geology/stonemine_tropic.xml</Actor>
</VisualActor>

View File

@ -10,6 +10,9 @@
<Obstruction>
<Static width="7.0" depth="7.0"/>
</Obstruction>
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<Selectable>
<EditorOnly disable=""/>
<Overlay>

View File

@ -7,9 +7,6 @@
<Obstruction>
<Static width="13.0" depth="13.0"/>
</Obstruction>
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<ResourceSupply>
<Amount>5000</Amount>
<MaxGatherers>24</MaxGatherers>

View File

@ -7,9 +7,6 @@
<Obstruction>
<Static width="13.0" depth="13.0"/>
</Obstruction>
<Position>
<Anchor>pitch-roll</Anchor>
</Position>
<ResourceSupply>
<Amount>5000</Amount>
<MaxGatherers>24</MaxGatherers>

View File

@ -42,6 +42,9 @@
<Obstruction>
<Unit radius="1.0"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Selectable>
<Overlay>
<Texture>

View File

@ -31,6 +31,9 @@
<Obstruction>
<Unit radius="4.0"/>
</Obstruction>
<Position>
<Anchor>pitch</Anchor>
</Position>
<Selectable>
<Overlay>
<Texture>

View File

@ -38,6 +38,9 @@
<stone>10</stone>
<metal>10</metal>
</Loot>
<Position>
<Anchor>pitch</Anchor>
</Position>
<ResourceDropsite>
<Types>food wood stone metal</Types>
</ResourceDropsite>

View File

@ -30,6 +30,7 @@
#include "maths/MathUtil.h"
#include "maths/Matrix3D.h"
#include "maths/Vector3D.h"
#include "maths/Vector2D.h"
#include "ps/CLogger.h"
/**
@ -58,6 +59,7 @@ public:
UPRIGHT = 0,
PITCH = 1,
PITCH_ROLL = 2,
ROLL=3,
} m_AnchorType;
bool m_Floating;
@ -73,7 +75,12 @@ public:
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
// not serialized:
float m_InterpolatedRotX, m_InterpolatedRotY, m_InterpolatedRotZ;
float m_LastInterpolatedRotX, m_LastInterpolatedRotZ; // not serialized
bool m_NeedInitialXZRotation;
static std::string GetSchema()
{
@ -87,9 +94,10 @@ public:
"</a:example>"
"<element name='Anchor' a:help='Automatic rotation to follow the slope of terrain'>"
"<choice>"
"<value a:help='Always stand straight up'>upright</value>"
"<value a:help='Rotate backwards and forwards to follow the terrain'>pitch</value>"
"<value a:help='Rotate in all direction to follow the terrain'>pitch-roll</value>"
"<value a:help='Always stand straight up (e.g. humans)'>upright</value>"
"<value a:help='Rotate backwards and forwards to follow the terrain (e.g. animals)'>pitch</value>"
"<value a:help='Rotate sideways to follow the terrain'>roll</value>"
"<value a:help='Rotate in all directions to follow the terrain (e.g. carts)'>pitch-roll</value>"
"</choice>"
"</element>"
"<element name='Altitude' a:help='Height above terrain in metres'>"
@ -110,6 +118,8 @@ public:
m_AnchorType = PITCH;
else if (anchor == L"pitch-roll")
m_AnchorType = PITCH_ROLL;
else if (anchor == L"roll")
m_AnchorType = ROLL;
else
m_AnchorType = UPRIGHT;
@ -122,7 +132,10 @@ public:
m_RotYSpeed = paramNode.GetChild("TurnRate").ToFixed().ToFloat();
m_RotX = m_RotY = m_RotZ = entity_angle_t::FromInt(0);
m_InterpolatedRotY = 0;
m_InterpolatedRotX = m_InterpolatedRotY = m_InterpolatedRotZ = 0.f;
m_LastInterpolatedRotX = m_LastInterpolatedRotZ = 0.f;
m_NeedInitialXZRotation = false;
}
virtual void Deinit()
@ -138,8 +151,6 @@ public:
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);
@ -152,9 +163,22 @@ public:
const char* anchor = "???";
switch (m_AnchorType)
{
case UPRIGHT: anchor = "upright"; break;
case PITCH: anchor = "pitch"; break;
case PITCH_ROLL: anchor = "pitch-roll"; break;
case PITCH:
anchor = "pitch";
break;
case PITCH_ROLL:
anchor = "pitch-roll";
break;
case ROLL:
anchor = "roll";
break;
case UPRIGHT: // upright is the default
default:
anchor = "upright";
break;
}
serialize.StringASCII("anchor", anchor, 0, 16);
serialize.Bool("floating", m_Floating);
@ -181,6 +205,9 @@ public:
// TODO: should there be range checks on all these values?
m_InterpolatedRotY = m_RotY.ToFloat();
if (m_InWorld)
UpdateXZRotation();
}
virtual bool IsInWorld()
@ -216,6 +243,11 @@ public:
m_LastZ = m_PrevZ = m_Z = z;
m_InWorld = true;
UpdateXZRotation();
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
AdvertisePositionChanges();
}
@ -329,6 +361,14 @@ public:
m_RotY = y;
m_InterpolatedRotY = m_RotY.ToFloat();
if (m_InWorld)
{
UpdateXZRotation();
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
}
AdvertisePositionChanges();
}
@ -337,6 +377,14 @@ public:
m_RotX = x;
m_RotZ = z;
if (m_InWorld)
{
UpdateXZRotation();
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
}
AdvertisePositionChanges();
}
@ -400,11 +448,13 @@ public:
float y = baseY + m_YOffset.ToFloat();
// TODO: do something with m_AnchorType
CMatrix3D m;
m.SetXRotation(m_RotX.ToFloat());
m.RotateZ(m_RotZ.ToFloat());
CMatrix3D m;
// linear interpolation is good enough (for RotX/Z).
// As you always stay close to zero angle.
m.SetXRotation(Interpolate(m_LastInterpolatedRotX, m_InterpolatedRotX, frameOffset));
m.RotateZ(Interpolate(m_LastInterpolatedRotZ, m_InterpolatedRotZ, frameOffset));
m.RotateY(rotY + (float)M_PI);
m.Translate(CVector3D(x, y, z));
@ -420,32 +470,60 @@ public:
const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (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.deltaSimTime, +m_RotYSpeed*msgData.deltaSimTime);
// 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;
if (rotY != m_InterpolatedRotY)
{
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.deltaSimTime, +m_RotYSpeed*msgData.deltaSimTime);
// 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;
// update the visual XZ rotation
if (m_InWorld)
{
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
UpdateXZRotation();
}
}
if (m_InWorld && m_NeedInitialXZRotation)
{
// the terrain probably wasn't loaded last time we tried, so update the XZ rotation without interpolation
UpdateXZRotation();
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
}
break;
}
case MT_TurnStart:
{
m_LastInterpolatedRotX = m_InterpolatedRotX;
m_LastInterpolatedRotZ = m_InterpolatedRotZ;
if (m_InWorld && (m_LastX != m_X || m_LastZ != m_Z))
UpdateXZRotation();
// 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()
@ -461,6 +539,56 @@ private:
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
}
void UpdateXZRotation()
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::UpdateXZRotation called on entity when IsInWorld is false");
return;
}
if (!m_RotZ.IsZero() || !m_RotX.IsZero())
{
// set the visual rotations to the ones fixed by the interface
m_InterpolatedRotX = m_RotX.ToFloat();
m_InterpolatedRotZ = m_RotZ.ToFloat();
return;
}
// change nothing if anchor is upright
if (m_AnchorType == UPRIGHT)
return;
CmpPtr<ICmpTerrain> cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
if (!cmpTerrain || !cmpTerrain->IsLoaded())
{
// try again when terrain is loaded
m_NeedInitialXZRotation = true;
return;
}
// TODO average normal (average all the tiles?) for big units or for buildings
CVector3D normal = cmpTerrain->CalcNormal(m_X, m_Z);
// rotate the normal so the positive x direction is in the direction of the unit
CVector2D projected = CVector2D(normal.X, normal.Z);
projected.Rotate(m_InterpolatedRotY);
normal.X = projected.X;
normal.Z = projected.Y;
// project and calculate the angles
if (m_AnchorType == PITCH || m_AnchorType == PITCH_ROLL)
m_InterpolatedRotX = -atan2(normal.Z, normal.Y);
if (m_AnchorType == ROLL || m_AnchorType == PITCH_ROLL)
m_InterpolatedRotZ = atan2(normal.X, normal.Y);
m_NeedInitialXZRotation = false;
return;
}
};
REGISTER_COMPONENT_TYPE(Position)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2011 Wildfire Games.
/* Copyright (C) 2013 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -44,8 +44,12 @@ class CMatrix3D;
* - 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)
* - Pitch-Roll (rotates in all directions to match the terrain, e.g. for carts)
* - Roll (rotates sideways to match the terrain)
* NOTE: terrain conformance is currently only a local, visual effect; it doesn't change
* the network synchronized rotation or the data returned by GetRotation
* - Rotation around relative X (pitch), Z (roll) axes (rare; used for special effects)
* NOTE: if XZ rotation is non-zero, it will override the terrain conformance mode
*
* Entities can also be 'outside the world' (e.g. hidden inside a building), in which
* case they have no position. Callers <b>must</b> check the entity is in the world, before
@ -137,6 +141,7 @@ public:
* 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.
* @note if either x or z is non-zero, it will override terrain conformance mode
*/
virtual void SetXZRotation(entity_angle_t x, entity_angle_t z) = 0;