#pragma once #include #ifdef BUILD_TESTING #include #endif #include "direction.hpp" #include "size.hpp" #include "utils/stdcompat/abs.hpp" namespace devilution { struct Displacement { int deltaX; int deltaY; Displacement() = default; constexpr Displacement(int deltaX, int deltaY) : deltaX(deltaX) , deltaY(deltaY) { } explicit constexpr Displacement(int delta) : deltaX(delta) , deltaY(delta) { } explicit constexpr Displacement(const Size &size) : deltaX(size.width) , deltaY(size.height) { } explicit constexpr Displacement(Direction direction) : Displacement(fromDirection(direction)) { } constexpr bool operator==(const Displacement &other) const { return deltaX == other.deltaX && deltaY == other.deltaY; } constexpr bool operator!=(const Displacement &other) const { return !(*this == other); } constexpr Displacement &operator+=(const Displacement &displacement) { deltaX += displacement.deltaX; deltaY += displacement.deltaY; return *this; } constexpr Displacement &operator-=(const Displacement &displacement) { deltaX -= displacement.deltaX; deltaY -= displacement.deltaY; return *this; } constexpr Displacement &operator*=(const int factor) { deltaX *= factor; deltaY *= factor; return *this; } constexpr Displacement &operator*=(const float factor) { deltaX = static_cast(deltaX * factor); deltaY = static_cast(deltaY * factor); return *this; } constexpr Displacement &operator/=(const int factor) { deltaX /= factor; deltaY /= factor; return *this; } constexpr Displacement &operator/=(const float factor) { deltaX = static_cast(deltaX / factor); deltaY = static_cast(deltaY / factor); return *this; } constexpr friend Displacement operator+(Displacement a, Displacement b) { a += b; return a; } constexpr friend Displacement operator-(Displacement a, Displacement b) { a -= b; return a; } constexpr friend Displacement operator*(Displacement a, const int factor) { a *= factor; return a; } constexpr friend Displacement operator*(Displacement a, const float factor) { a *= factor; return a; } constexpr friend Displacement operator/(Displacement a, const int factor) { a /= factor; return a; } constexpr friend Displacement operator/(Displacement a, const float factor) { a /= factor; return a; } constexpr friend Displacement operator-(const Displacement &a) { return { -a.deltaX, -a.deltaY }; } constexpr friend Displacement operator<<(Displacement a, unsigned factor) { return { a.deltaX << factor, a.deltaY << factor }; } constexpr friend Displacement operator>>(Displacement a, unsigned factor) { return { a.deltaX >> factor, a.deltaY >> factor }; } constexpr friend Displacement abs(Displacement a) { return { abs(a.deltaX), abs(a.deltaY) }; } float magnitude() const { return static_cast(hypot(deltaX, deltaY)); } /** * @brief Returns a new Displacement object in screen coordinates. * * Transforming from world space to screen space involves a rotation of -135° and scaling to fit within a 64x32 pixel tile (since Diablo uses isometric projection) * 32 and 16 are used as the x/y scaling factors being half the relevant max dimension, the rotation matrix is [[-, +], [-, -]] as sin(-135°) = cos(-135°) = ~-0.7. * * [-32, 32] [dx] = [-32dx + 32dy] = [ 32dy - 32dx ] = [ 32(dy - dx)] * [-16, -16] [dy] = [-16dx + -16dy] = [-(16dy + 16dx)] = [-16(dy + dx)] * * @return A representation of the original displacement in screen coordinates. */ constexpr Displacement worldToScreen() const { return { (deltaY - deltaX) * 32, (deltaY + deltaX) * -16 }; } /** * @brief Returns a new Displacement object in world coordinates. * * This is an inverse matrix of the worldToScreen transformation. * * @return A representation of the original displacement in world coordinates. */ constexpr Displacement screenToWorld() const { return { (2 * deltaY + deltaX) / -64, (2 * deltaY - deltaX) / -64 }; } /** * @brief Missiles flip the axes for some reason -_- * @return negated world displacement, for use with missile movement routines. */ constexpr Displacement screenToMissile() const { return -screenToWorld(); } constexpr Displacement screenToLight() const { return { (2 * deltaY + deltaX) / 8, (2 * deltaY - deltaX) / 8 }; } /** * @brief Returns a 16 bit fixed point normalised displacement in isometric projection * * This will return a displacement of the form (-1.0 to 1.0, -0.5 to 0.5), to get a full tile offset you can multiply by 16 */ Displacement worldToNormalScreen() const { // Most transformations between world and screen space take shortcuts when scaling to simplify the math. This // routine is typically used with missiles where we want a normal vector that can be multiplied with a target // velocity (given in pixels). We could normalize the vector first but then we'd need to scale it during // rotation from world to screen space. To save performing unnecessary divisions we rotate first without // correcting the scaling. This gives a vector in elevation projection aligned with screen space. Displacement rotated { (deltaY - deltaX), -(deltaY + deltaX) }; // then normalize this vector Displacement rotatedAndNormalized = rotated.normalized(); // and finally scale the y axis to bring it to isometric projection return { rotatedAndNormalized.deltaX, rotatedAndNormalized.deltaY / 2 }; } /** * @brief Calculates a 16 bit fixed point normalized displacement (having magnitude of ~1.0) from the current Displacement */ Displacement normalized() const { float magnitude = this->magnitude(); Displacement normalDisplacement = *this << 16; normalDisplacement /= magnitude; return normalDisplacement; } constexpr Displacement Rotate(int quadrants) { constexpr int Sines[] = { 0, 1, 0, -1 }; quadrants = (quadrants % 4 + 4) % 4; int sine = Sines[quadrants]; int cosine = Sines[(quadrants + 1) % 4]; return Displacement { deltaX * cosine - deltaY * sine, deltaX * sine + deltaY * cosine }; } #ifdef BUILD_TESTING /** * @brief Format displacements nicely in test failure messages * @param stream output stream, expected to have overloads for int and char* * @param offset Object to display * @return the stream, to allow chaining */ friend std::ostream &operator<<(std::ostream &stream, const Displacement &offset) { return stream << "(x: " << offset.deltaX << ", y: " << offset.deltaY << ")"; } #endif private: static constexpr Displacement fromDirection(Direction direction) { switch (direction) { case Direction::South: return { 1, 1 }; case Direction::SouthWest: return { 0, 1 }; case Direction::West: return { -1, 1 }; case Direction::NorthWest: return { -1, 0 }; case Direction::North: return { -1, -1 }; case Direction::NorthEast: return { 0, -1 }; case Direction::East: return { 1, -1 }; case Direction::SouthEast: return { 1, 0 }; default: return { 0, 0 }; } }; }; } // namespace devilution