LCOV - code coverage report
Current view: top level - test - object.cpp (source / functions) Coverage Total Hit
Test: coverage.info Lines: 97.8 % 231 226
Test Date: 2026-04-03 02:26:39 Functions: 50.0 % 12 6

            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 : }
        

Generated by: LCOV version 2.4-0