Line data Source code
1 : #include "catch.hpp"
2 : #include "Object.h"
3 : #include "Quaternion.h"
4 : #include <cmath>
5 :
6 : class BasicObject : public Object {
7 : public:
8 0 : void draw(Object* o) {}
9 0 : void drawOutline(Object* o) {}
10 0 : void handleCollision(Object* o) {}
11 0 : void update(Vector* v) {}
12 : void animate(float f, Object* o) {}
13 0 : void printTypeName(void) {}
14 : };
15 :
16 31 : SCENARIO( "Object Creation", "[object]" ) {
17 60 : GIVEN( "A basic derived object" ) {
18 30 : BasicObject o;
19 :
20 41 : WHEN( "first initiated" ) {
21 12 : THEN( "it is set to active" ) {
22 1 : REQUIRE(o.getActive());
23 1 : }
24 :
25 12 : THEN( "it is set to 'NORMAL'" ) {
26 1 : REQUIRE(o.getState() == NORMAL);
27 1 : }
28 :
29 12 : THEN( "it has no velocity" ) {
30 1 : Vector v = o.getVelocity();
31 1 : REQUIRE(v.x == 0.0f);
32 1 : REQUIRE(v.y == 0.0f);
33 1 : REQUIRE(v.z == 0.0f);
34 1 : }
35 :
36 12 : THEN( "it has no speed" ) {
37 1 : Vector v = o.getSpeed();
38 1 : REQUIRE(v.x == 0.0f);
39 1 : REQUIRE(v.y == 0.0f);
40 1 : REQUIRE(v.z == 0.0f);
41 1 : }
42 :
43 12 : THEN( "it is scaled at 1x in all directions" ) {
44 1 : Vector v = o.getScale();
45 1 : REQUIRE(v.x == 1.0f);
46 1 : REQUIRE(v.y == 1.0f);
47 1 : REQUIRE(v.z == 1.0f);
48 1 : }
49 :
50 12 : THEN( "it is positioned at the origin" ) {
51 1 : Vector v = o.getPosition();
52 1 : REQUIRE(v.x == 0.0f);
53 1 : REQUIRE(v.y == 0.0f);
54 1 : REQUIRE(v.z == 0.0f);
55 1 : }
56 :
57 12 : THEN( "it is in view" ) {
58 1 : REQUIRE(o.getInView() == true);
59 1 : }
60 :
61 12 : THEN( "draw is enabled" ) {
62 1 : REQUIRE(o.getDrawEnabled() == true);
63 1 : }
64 :
65 12 : THEN( "collision detection is enabled" ) {
66 1 : REQUIRE(o.getCollisionDetectionEnabled() == true);
67 1 : }
68 :
69 12 : THEN( "it has a negative type ID" ) {
70 1 : REQUIRE(o.getTypeId() == -1);
71 1 : }
72 :
73 12 : THEN( "it has no script" ) {
74 1 : REQUIRE(o.getScriptName() == "");
75 1 : }
76 11 : }
77 :
78 31 : WHEN( "calcTriangleCenter is called" ) {
79 1 : const float h = o.calcTriangleCenter(0.0f, 4.0f, 8.0f);
80 2 : THEN( "it uses successive midpoint weighting (not arithmetic mean)" ) {
81 1 : const float th1 = 0.5f * 0.0f + 0.5f * 4.0f;
82 1 : const float expected = 0.5f * th1 + 0.5f * 8.0f;
83 1 : REQUIRE(h == Approx(expected));
84 1 : REQUIRE(h == Approx(5.0f));
85 1 : }
86 1 : }
87 :
88 31 : WHEN( "kinematic setters are used" ) {
89 1 : o.setPosition(1.0f, -2.0f, 3.0f);
90 1 : o.setVelocity(0.25f, 0.5f, 0.125f);
91 1 : o.setSpeed(-1.0f, 0.0f, 2.0f);
92 1 : o.setScale(2.0f, 3.0f, 4.0f);
93 1 : o.setRotationVelocity(0.1f, -0.2f, 0.3f);
94 1 : Vector p = o.getPosition();
95 1 : Vector v = o.getVelocity();
96 1 : Vector sp = o.getSpeed();
97 1 : Vector sc = o.getScale();
98 1 : Vector rv = o.getRotationVelocity();
99 :
100 2 : THEN( "getters reflect the values" ) {
101 1 : REQUIRE(p.x == Approx(1.0f));
102 1 : REQUIRE(p.y == Approx(-2.0f));
103 1 : REQUIRE(p.z == Approx(3.0f));
104 1 : REQUIRE(v.x == Approx(0.25f));
105 1 : REQUIRE(sp.x == Approx(-1.0f));
106 1 : REQUIRE(sp.z == Approx(2.0f));
107 1 : REQUIRE(sc.x == Approx(2.0f));
108 1 : REQUIRE(rv.y == Approx(-0.2f));
109 1 : }
110 1 : }
111 :
112 31 : WHEN( "Vector overloads for position and velocity" ) {
113 1 : Vector pos(9.0f, 8.0f, 7.0f);
114 1 : Vector vel(1.0f, 1.0f, 1.0f);
115 1 : o.setPosition(pos);
116 1 : o.setVelocity(vel);
117 1 : Vector p = o.getPosition();
118 1 : Vector v = o.getVelocity();
119 2 : THEN( "they copy component-wise" ) {
120 1 : REQUIRE(p.x == Approx(9.0f));
121 1 : REQUIRE(v.z == Approx(1.0f));
122 1 : }
123 1 : }
124 :
125 31 : WHEN( "setRotationEuler updates rotation quaternion" ) {
126 1 : o.setRotationEuler(0.2f, -0.1f, 0.3f);
127 1 : Quaternion q = o.getRotation();
128 2 : THEN( "rotation is non-trivial unit quaternion" ) {
129 1 : const float len = std::sqrt(
130 1 : q.getX() * q.getX() + q.getY() * q.getY() + q.getZ() * q.getZ() + q.getW() * q.getW());
131 1 : REQUIRE(len == Approx(1.0f));
132 1 : }
133 1 : }
134 :
135 31 : WHEN( "collision detection is disabled" ) {
136 1 : o.setCollisionDetectionEnabled(false);
137 2 : THEN( "getCollisionDetectionEnabled returns false" ) {
138 1 : REQUIRE(o.getCollisionDetectionEnabled() == false);
139 1 : }
140 1 : }
141 :
142 31 : WHEN( "misc identity and bookkeeping fields" ) {
143 1 : o.setActive(false);
144 1 : o.setTypeId(42);
145 1 : o.setGameLevelId(7u);
146 1 : o.setScaleRate(0.1f, 0.2f, 0.3f);
147 1 : o.setIsAlwaysLit(true);
148 1 : Quaternion q;
149 1 : q.loadMultIdentity();
150 1 : o.setRotationQuaternion(q);
151 1 : o.setValInterpBegin(0.0f);
152 1 : o.setValInterpCurrent(5.0f);
153 1 : o.setValInterpEnd(10.0f);
154 :
155 2 : THEN( "getters match" ) {
156 1 : REQUIRE(o.getActive() == false);
157 1 : REQUIRE(o.getTypeId() == 42);
158 1 : REQUIRE(o.getGameLevelId() == 7u);
159 1 : Vector sr = o.getScaleRate();
160 1 : REQUIRE(sr.x == Approx(0.1f));
161 1 : REQUIRE(o.getIsAlwaysLit() == true);
162 1 : REQUIRE(o.getRotation().getW() == Approx(1.0f));
163 1 : REQUIRE(o.getValInterpCurrent() == Approx(5.0f));
164 1 : REQUIRE(o.getValInterpEnd() == Approx(10.0f));
165 1 : }
166 1 : }
167 :
168 31 : WHEN( "interpolation is toggled" ) {
169 1 : o.setIsInterpolationEnabled(true);
170 2 : THEN( "isInterpolationEnabled returns true" ) {
171 1 : REQUIRE(o.isInterpolationEnabled() == true);
172 1 : }
173 1 : }
174 :
175 31 : WHEN( "interpolation is disabled" ) {
176 1 : o.setIsInterpolationEnabled(false);
177 2 : THEN( "isInterpolationEnabled returns false" ) {
178 1 : REQUIRE(o.isInterpolationEnabled() == false);
179 1 : }
180 1 : }
181 :
182 : // rotationChanged_ initialises to true so the rotation matrix is built on the first frame.
183 31 : WHEN( "rotationChanged latch is cleared then re-read" ) {
184 1 : o.setRotationChanged(false);
185 2 : THEN( "isRotationChanged returns false" ) {
186 1 : REQUIRE(o.isRotationChanged() == false);
187 1 : }
188 1 : }
189 :
190 31 : WHEN( "default collision sphere test flag" ) {
191 2 : THEN( "generic objects enable sphere test" ) {
192 1 : REQUIRE(o.getEnableSphereTest() == true);
193 1 : }
194 1 : }
195 :
196 : // setRotation() sets rotationChanged_ = true as a side effect; setRotationQuaternion()
197 : // does not. Callers that need the rotation matrix rebuilt must use setRotation().
198 31 : WHEN( "setRotationQuaternion is called after clearing rotationChanged" ) {
199 1 : Quaternion q;
200 1 : q.loadMultIdentity();
201 1 : o.setRotationChanged(false);
202 1 : o.setRotationQuaternion(q);
203 :
204 2 : THEN( "rotationChanged remains false" ) {
205 1 : REQUIRE(o.isRotationChanged() == false);
206 1 : }
207 1 : }
208 :
209 31 : WHEN( "setRotation is called after clearing rotationChanged" ) {
210 1 : Quaternion q;
211 1 : q.loadMultIdentity();
212 1 : o.setRotationChanged(false);
213 1 : o.setRotation(q);
214 :
215 2 : THEN( "rotationChanged is set to true" ) {
216 1 : REQUIRE(o.isRotationChanged() == true);
217 1 : }
218 1 : }
219 :
220 : // processCollisions is not tested here: it requires two objects chained via ObjectNode::next
221 : // and has a known radius bug (radius2 uses this object's boundingSphere_ instead of the
222 : // colliding object's sphere, so both radii come from the same getter).
223 :
224 : // Object::animate drives the frame loop. BasicObject overrides animate(float, Object*)
225 : // with a different signature, so Object::animate must be called explicitly to reach
226 : // the base implementation.
227 31 : WHEN( "Object::animate is called with a non-zero velocity" ) {
228 1 : o.setPosition(0.0f, 0.0f, 0.0f);
229 1 : o.setVelocity(2.0f, 0.0f, -1.0f);
230 1 : o.Object::animate(0.5f, nullptr);
231 1 : Vector p = o.getPosition();
232 :
233 2 : THEN( "position advances by velocity * elapsed time" ) {
234 1 : REQUIRE(p.x == Approx(1.0f)); // 0 + 2.0 * 0.5
235 1 : REQUIRE(p.y == Approx(0.0f)); // velocity_.y == 0, unchanged
236 1 : REQUIRE(p.z == Approx(-0.5f)); // 0 + (-1.0) * 0.5
237 1 : }
238 1 : }
239 :
240 : // With non-zero rotationVelocity and interpolation disabled (the default), animate
241 : // accumulates rotation_ by rotationVelocity * timeElapsed and sets rotationChanged_.
242 32 : WHEN( "Object::animate is called with a non-zero rotation velocity" ) {
243 2 : o.setRotationChanged(false);
244 2 : o.setRotationVelocity(0.0f, 1.0f, 0.0f);
245 2 : o.Object::animate(0.5f, nullptr);
246 :
247 3 : THEN( "rotationChanged is set" ) {
248 1 : REQUIRE(o.isRotationChanged() == true);
249 1 : }
250 :
251 3 : THEN( "rotation is no longer identity" ) {
252 1 : Quaternion q = o.getRotation();
253 1 : const float len = std::sqrt(
254 2 : q.getX() * q.getX() + q.getY() * q.getY() +
255 1 : q.getZ() * q.getZ() + q.getW() * q.getW());
256 1 : REQUIRE(len == Approx(1.0f));
257 : // y-axis rotation: x and z components stay near zero, w < 1
258 1 : REQUIRE(q.getW() < 1.0f);
259 1 : }
260 2 : }
261 :
262 : // isDrawable guards every draw call: active && inView && drawEnabled && state != DEAD.
263 : // inView has no public setter so only the default (true) can be tested here.
264 31 : WHEN( "isDrawable is checked in default state" ) {
265 2 : THEN( "object is drawable" ) {
266 1 : REQUIRE(o.isDrawable() == true);
267 1 : }
268 1 : }
269 :
270 31 : WHEN( "object is deactivated" ) {
271 1 : o.setActive(false);
272 2 : THEN( "isDrawable returns false" ) {
273 1 : REQUIRE(o.isDrawable() == false);
274 1 : }
275 1 : }
276 :
277 31 : WHEN( "draw is disabled" ) {
278 1 : o.setDrawEnabled(false);
279 2 : THEN( "isDrawable returns false" ) {
280 1 : REQUIRE(o.isDrawable() == false);
281 1 : }
282 1 : }
283 :
284 : // setState(DEAD) does not clear active_ — callers must also call setActive(false).
285 31 : WHEN( "state is set to DEAD" ) {
286 1 : o.setState(DEAD);
287 2 : THEN( "isDrawable returns false even though active_ is unchanged" ) {
288 1 : REQUIRE(o.getActive() == true);
289 1 : REQUIRE(o.isDrawable() == false);
290 1 : }
291 1 : }
292 30 : }
293 30 : }
|