#include "uox3.h" #include "regions.h" #include "cEffects.h" #include #include constexpr auto MAX_COLLISIONS = 1024; constexpr auto LOSXYMAX = 256; // Maximum items UOX3 can handle on one X/Y square // Contains all current line of sight functionality /* PROPOSED 4 pass Line of Sight algorithm that will take into account current issues. Note that it may actually be more suited to a client than server. Line of Sight algorithm 4 pass algorithm Pass 1: Map Pass 2: Statics Pass 3: Dynamic Items Pass 4: Multis // this pass is often ignored, as multis are rare Failure at any pass returns false Flags: TREES and BUSHES WALLS_CHIMNEYS DOORS ROOFING_SLANTED FLOORS_FLAT_ROOFING LAVA_WATER NO_MAP ONLY if all tests run through to their conclusion will you ever return a true value General algorithm: // Different Xs Find dY Find dZ // Different Ys Find dX Find dZ // Same X/Y // Special case // Only pass 1 and 2 are different based on each of these cases Different Xs algorithm PASS 1 if map check For each x between src and trg Increment Y by dY Increment Z by dZ Get map tile at x/y If no overlap z, discard elseif flag no match, discard else continue next x PASS 2 For each x between src and trg Increment Y by dY Increment Z by dZ Get static tile at x/y If no overlap z, discard elseif flag no match, discard else continue next x Different Ys algorithm PASS 1 if map check for each y between src and trg Increment X by dX Increment Z by dZ Get map tile at x/y if no overlap z, discard elseif flag no match, discard else continue next y PASS 2 for each y between src and trg Increment X by dX Increment Z by dZ Get static tile at x/y if no overlap z, discard elseif flag no match, discard else continue next y Different Zs algorithm PASS 1 if map check Find tile at x/y if z is not between min z and max z, discard if flag overlap, blocked PASS 2 for each static tile at x/y if z is not between min z and max z, discard if flag overlap, blocked next tile PASS 3 For each item in the same map area if item.X < min X, discard if item.Y < min Y, discard if item.Z < min Z, discard if item.X > max X, discard if item.Y > min Y, discard if item.Z > min Z, discard Get item flags if no flag overlap, discard Construct 6 quads of the item Push quads onto list Next item For each quad in list Do intersect test of ray and quad if intersect, return false next quad PASS 4 For each item in the multi if item.X < min X, discard if item.Y < min Y, discard if item.Z < min Z, discard if item.X > max X, discard if item.Y > min Y, discard if item.Z > min Z, discard Get item flags if no flag overlap, discard Construct 6 quads of the item Push quads onto list Next item for each quad in list Do intersect test of ray and quad if intersect, return false next quad QUAD POLYGON CONSTRUCTION ALGORITHM Need to create 6 polys One polygon per side of rectangular prism Need to use x/y offset Also need to use height of tile from tiledata.mul */ //================================================================================================== // Structures used struct Vector2D_st { R32 x; R32 y; Vector2D_st(): x( 0.0 ), y( 0.0 ) { } Vector2D_st( R32 X, R32 Y ) : x( X ), y( Y ) { } }; //================================================================================================== struct Vector3D_st { SI32 x; SI32 y; SI08 z; Vector3D_st(): x( 0 ), y( 0 ), z( 0 ) { } Vector3D_st( SI32 X, SI32 Y, SI08 Z ) : x( X ), y( Y ), z( Z ) { } }; //================================================================================================== inline bool operator == ( Vector3D_st const &a, Vector3D_st const &b ) { return (( a.x == b.x ) && ( a.y == b.y ) && ( a.z == b.z )); } //================================================================================================== inline bool operator < ( Vector3D_st const &a, Vector3D_st const &b ) { return (( a.x < b.x ) && ( a.y < b.y ) && ( a.z < b.z )); } //================================================================================================== struct Line2D_st { Vector2D_st loc; Vector2D_st dir; Line2D_st() { } Line2D_st( Vector2D_st LOC, Vector2D_st DIR ) : loc( LOC ), dir( DIR ) { } auto CollideLines2D( Line2D_st toCollide ) const ->Vector2D_st; }; //================================================================================================== inline auto Line2D_st::CollideLines2D( Line2D_st toCollide ) const ->Vector2D_st { if((( dir.x == 0 ) && ( toCollide.dir.x == 0 )) || (( dir.y == 0 ) && ( toCollide.dir.y == 0 ))) return Vector2D_st( -1.0f, -1.0f ); // error, parallel or invalid lines if((( dir.x * toCollide.dir.y ) - ( toCollide.dir.x * dir.y )) == 0 ) return Vector2D_st( -1.0f, -1.0f ); // error, parallel lines R32 t = 0.0f; // parameter of toCollide-line // linear evaluation of extended 2x2 matrix t = (((( loc.x - toCollide.loc.x ) * (- dir.y) ) + ( dir.x * ( loc.y - toCollide.loc.y ))) / (( dir.x * toCollide.dir.y ) - ( toCollide.dir.x * dir.y ))); return Vector2D_st(( toCollide.loc.x + t * toCollide.dir.x ), ( toCollide.loc.y + t * toCollide.dir.y )); } //================================================================================================== struct Line3D_st { Vector3D_st loc; Vector3D_st dir; Line3D_st() = default; Line3D_st( Vector3D_st LOC, Vector3D_st DIR ) : loc( LOC ), dir( DIR ) { } auto dzInDirectionX() const ->R32; auto dzInDirectionY() const ->R32; auto Projection2D() const ->Line2D_st; }; //================================================================================================== inline auto Line3D_st::dzInDirectionX() const ->R32 { if( dir.x == 0 ) { return static_cast( dir.z ); } return static_cast( dir.z ) / static_cast( dir.x ); } //================================================================================================== inline auto Line3D_st::dzInDirectionY() const ->R32 { if( dir.y == 0 ) { return static_cast( dir.z ); } return static_cast( dir.z ) / static_cast( dir.y ); } //================================================================================================== inline auto Line3D_st::Projection2D( void ) const ->Line2D_st { if(( dir.x == 0 ) && ( dir.y == 0 )) { return Line2D_st( Vector2D_st( -1.0f, -1.0f ), Vector2D_st( -1.0f, -1.0f )); } return Line2D_st( Vector2D_st( static_cast( loc.x ), static_cast( loc.y )), Vector2D_st( static_cast( dir.x ), static_cast( dir.y ))); } //o------------------------------------------------------------------------------------------------o //| Function - MapTileBlocks() //o------------------------------------------------------------------------------------------------o //| Purpose - Check if maptile blocks Line of Sight //o------------------------------------------------------------------------------------------------o bool MapTileBlocks( [[maybe_unused]] CSocket *mSock, bool nostatic, Line3D_st LoS, SI16 x1, SI16 y1, [[maybe_unused]] SI08 z, SI16 x2, SI16 y2, UI08 worldNum, SI08 z2Top ) { // Map tile at previous coordinate along the LoS path auto srcMap = Map->SeekMap( x1, y1, worldNum ); // Map tile at next coordinate along the LoS path auto trgMap = Map->SeekMap( x2, y2, worldNum ); if( srcMap.terrainInfo == nullptr || trgMap.terrainInfo == nullptr ) return true; // Get tileIDs for previous tile in LoS path, and next one const UI16 mID1 = srcMap.tileId; const UI16 mID2 = trgMap.tileId; // Continue if neither of the two tiles is a NoDraw tile, or a cave entrance tile if(( mID1 != 2 && mID2 != 2 ) && ( mID1 != 475 && mID2 != 475 )) { // Get z for previous tile in LoS path, and for next one auto mz1 = srcMap.altitude; auto mz2 = trgMap.altitude; auto startLocZ = LoS.loc.z; // Check if LoS intersects with map/mountain walls //if(( mz1 < mz2 && z2Top <= mz2 && z >= mz1 ) || // 1) Collides with a map "wall" // ( mz1 > mz2 && z2Top <= mz1 && z >= mz2 ) || /*if((( startLocZ > mz1 && ( z < mz1 && z2Top < mz1 )) || ( startLocZ < mz1 && z > mz1 )) || (( startLocZ > mz2 && ( z < mz2 && z2Top < mz2 )) || ( startLocZ < mz2 && z > mz2 )) || ( startLocZ > mz1 && ( z2Top < mz1 )) || ( startLocZ > mz2 && ( z2Top < mz2 )) ||*/ //( z == mz1 && LoS.dir.z != 0 ) || // 2) Cuts a map "floor" if(( startLocZ > mz2 && z2Top < mz2 ) || ( startLocZ < mz2 && z2Top > mz2 ) || ( startLocZ > mz1 && ( mz1 < mz2 && ( mz2 > z2Top ))) || ( nostatic && // Ensure there is no static item (( mID1 >= 431 && mID1 <= 432 ) || ( mID1 >= 467 && mID1 <= 474 ) || ( mID1 >= 543 && mID1 <= 560 ) || ( mID1 >= 1754 && mID1 <= 1757 ) || ( mID1 >= 1787 && mID1 <= 1789 ) || ( mID1 >= 1821 && mID1 <= 1824 ) || ( mID1 >= 1851 && mID1 <= 1854 ) || ( mID1 >= 1881 && mID1 <= 1884 )))) // 3) Cuts a mountain { return true; } } return false; } //o------------------------------------------------------------------------------------------------o //| Function - CheckFlags() //o------------------------------------------------------------------------------------------------o //| Purpose - Check tiledata flags for tile //o------------------------------------------------------------------------------------------------o bool CheckFlags( UI08 typeToCheck, Tile_st &toCheck, SI08 startZ, SI08 destZ, bool useSurfaceZ ) { switch( typeToCheck ) { case TREES_BUSHES: // Trees, Shrubs, bushes - if it's blocking but has neither of the flags listed below, assume it's a tree! :P if( toCheck.CheckFlag( TF_FOLIAGE ) || (( toCheck.CheckFlag( TF_BLOCKING ) && !toCheck.CheckFlag( TF_WALL ) && !toCheck.CheckFlag( TF_SURFACE ) && !toCheck.CheckFlag( TF_WINDOW )) || ( !toCheck.CheckFlag( TF_CLIMBABLE ) && !toCheck.CheckFlag( TF_WET ) && !toCheck.CheckFlag( TF_ROOF )) || !toCheck.CheckFlag( TF_CONTAINER ))) return false; break; case WALLS_CHIMNEYS: // Walls, Chimneys, ovens, not fences if(( toCheck.CheckFlag( TF_WALL ) || toCheck.CheckFlag( TF_WINDOW )) && ( toCheck.CheckFlag( TF_BLOCKING ) || toCheck.CheckFlag( TF_NOSHOOT )) && !toCheck.CheckFlag( TF_SURFACE )) return true; break; case DOORS: // Doors, not gates if( toCheck.CheckFlag( TF_DOOR )) return true; break; case ROOFING_SLANTED: // Roofing Slanted if( toCheck.CheckFlag( TF_ROOF )) return true; break; case FLOORS_FLAT_ROOFING: // Floors & Flat Roofing (Attacking through floors Roofs) if( toCheck.CheckFlag( TF_SURFACE )) { if( useSurfaceZ ? ( startZ != destZ ) : (( startZ - 15) != destZ )) { return true; } } break; case LAVA_WATER: // Lava, water if( toCheck.CheckFlag( TF_WET )) return true; break; default: break; } return false; } SI08 GetSGN( SI16 startLoc, SI16 destLoc, SI16 &l1, SI16 &l2 ) { if( startLoc < destLoc ) { l1 = startLoc; l2 = destLoc; return 1; } else { l1 = destLoc; l2 = startLoc; if( startLoc > destLoc ) return -1; } return 0; } //o------------------------------------------------------------------------------------------------o //| Function - DynamicCanBlock() //o------------------------------------------------------------------------------------------------o //| Purpose - Check if dynamic item will block Line of Sight //o------------------------------------------------------------------------------------------------o UI16 DynamicCanBlock( CItem *toCheck, Vector3D_st *collisions, SI32 collisioncount, SI16 distX, SI16 distY, SI16 x1, SI16 x2, SI16 y1, SI16 y2, SI32 dz ) { const SI16 curX = toCheck->GetX(); const SI16 curY = toCheck->GetY(); const SI08 curZ = toCheck->GetZ(); SI32 i = 0; Vector3D_st *checkLoc = nullptr; if( !toCheck->CanBeObjType( OT_MULTI )) { if( toCheck->GetVisible() == VT_VISIBLE && curX >= x1 && curX <= x2 && curY >= y1 && curY <= y2 ) { CTile& iTile = Map->SeekTile( toCheck->GetId() ); for( i = 0; i < collisioncount; ++i ) { checkLoc = &collisions[i]; if( curX == checkLoc->x && curY == checkLoc->y && checkLoc->z >= curZ && checkLoc->z <= ( curZ + iTile.Height() )) return toCheck->GetId(); } } } else if( distX <= DIST_BUILDRANGE && distY <= DIST_BUILDRANGE ) { const UI16 multiId = static_cast( toCheck->GetId() - 0x4000 ); [[maybe_unused]] SI32 length = 0; if( !Map->MultiExists( multiId )) { Console << "LoS - Bad length in multi file. Avoiding stall" << myendl; auto map1 = Map->SeekMap( curX, curY, toCheck->WorldNumber() ); if( map1.terrainInfo != nullptr && map1.CheckFlag( TF_WET )) // is it water? { toCheck->SetId( 0x4001 ); } else { toCheck->SetId( 0x4064 ); } length = 0; } else { for( auto &multi : Map->SeekMulti( multiId ).items ) { if( multi.flag ) { const SI16 checkX = ( curX + multi.offsetX ); const SI16 checkY = ( curY + multi.offsetY ); if( checkX >= x1 && checkX <= x2 && checkY >= y1 && checkY <= y2 ) { const SI08 checkZ = ( curZ + multi.altitude ); CTile& multiTile = Map->SeekTile( multi.tileId ); for( i = 0; i < collisioncount; ++i ) { checkLoc = &collisions[i]; if( checkX == checkLoc->x && checkY == checkLoc->y && (( checkLoc->z >= checkZ && checkLoc->z <= (checkZ + multiTile.Height() )) || ( multiTile.Height() <= 2 && abs( checkLoc->z - checkZ ) <= dz ))) { return multi.tileId; } } } } } } } return INVALIDID; } //o------------------------------------------------------------------------------------------------o //| Function - LineOfSight() //| Date - 03 July, 2001 //| Changes - 18 March, 2002 //o------------------------------------------------------------------------------------------------o //| Purpose - Returns true if there is line of sight between src and trg //| //| Notes - Char (x1, y1, z1) is the char(pc/npc), Target (x2, y2, z2) is the target. //| s is for pc's, in case a message needs to be sent. //| the checkfor is what is checked for along the line of sight. //| Look at uox3.h to see options. Works like npc magic. //| //| #define TREES_BUSHES 1 // Trees and other large vegetaion in the way //| #define WALLS_CHIMNEYS 2 // Walls, chimineys, ovens, etc... in the way //| #define DOORS 4 // Doors in the way //| #define ROOFING_SLANTED 8 // So can't tele onto slanted roofs, basically //| #define FLOORS_FLAT_ROOFING 16 // For attacking between floors //| #define LAVA_WATER 32 // Don't know what all to use this for yet //| //| Just or (|) the values for the diff things together to get what to search for. //| So put in place of the paramater checkfor for example //| //| if( line_of_sight( s, x1, y1, z1, x2, y2, z2, WALLS_CHIMNEYS | DOORS | ROOFING_SLANTED )) //| //| it WAS based on the P.T., now its based on linear algebra;) //o------------------------------------------------------------------------------------------------o auto LineOfSight( CSocket *mSock, CChar *mChar, SI16 destX, SI16 destY, SI08 destZ, UI08 checkfor, bool useSurfaceZ, SI08 destZTop, bool checkDistance ) -> bool { const bool blocked = false; const bool not_blocked = true; if( destX == -1 && destY == -1 ) return not_blocked; // target canceled const SI16 startX = mChar->GetX(), startY = mChar->GetY(); const SI08 startZ = ( useSurfaceZ ? mChar->GetZ() : ( mChar->GetZ() + 15 )); // standard eye height of most bodies if useSurfaceZ is false, use feet height if true if(( startX == destX ) && ( startY == destY ) && ( startZ == destZ )) return not_blocked; // if source and target are on the same position const UI08 worldNumber = mChar->WorldNumber(); const UI16 instanceId = mChar->GetInstanceId(); const SI32 distX = abs( static_cast( destX - startX) ), distY = abs( static_cast( destY - startY )); const SI32 distZ = destZ - startZ; // abs( static_cast( destZ - startZ )); Line3D_st lineofsight = Line3D_st( Vector3D_st( startX, startY, startZ ), Vector3D_st( distX, distY, distZ )); const R64 rBlah = ( static_cast( distX ) * static_cast( distX )) + ( static_cast( distY ) * static_cast( distY )); const SI32 distance = static_cast( sqrt( rBlah )); // Let's provide some leeway based on height of object destZTop = destZ + destZTop; if( checkDistance && distance > 18 ) return blocked; //If target is next to us and within our field of view if( distance == 0 && destZ <= ( startZ + 3 ) && destZTop >= ( useSurfaceZ ? ( startZ ) : ( startZ - 15 ))) return not_blocked; std::vector vec; vec.resize( MAX_COLLISIONS ); auto collisions = vec.data(); SI16 x1, y1, x2, y2; SI32 i = 0; const SI08 sgn_x = GetSGN( startX, destX, x1, x2 ); const SI08 sgn_y = GetSGN( startY, destY, y1, y2 ); SI08 sgn_z = ( startZ < destZ ) ? 1 : ( -1 ); // signum for z if( startZ == destZ ) { sgn_z = 0; } // initialize array for( i = 0; i < ( distance * 2 ); ++i ) { collisions[i] = Vector3D_st( -1, -1, -1 ); } SI32 collisioncount = 0; R32 z_slope_vs_y = 0.0f; if( distY != 0 ) { z_slope_vs_y = static_cast( distZ ) / static_cast( distY ); } R32 z_slope_vs_x = 0.0f; if( distX != 0 ) { z_slope_vs_x = static_cast( distZ ) / static_cast( distX ); } SI32 dz = 0; if( distX > distY ) { dz = static_cast( floor( abs( z_slope_vs_x ))); } else { dz = static_cast( floor( abs( z_slope_vs_y ))); } if( sgn_x == 0 && sgn_y == 0 && sgn_z != 0 ) // should fix shooting through floor issues { for( i = 1; i <= abs( distZ ); ++i ) { collisions[collisioncount] = Vector3D_st( startX, startY, static_cast(startZ + ( i * sgn_z ))); ++collisioncount; } } else if( sgn_x == 0 ) // if we are on the same x-level, just push every x/y coordinate in y-direction from src to trg into the array { for( i = 1; i <= distY; ++i ) { collisions[collisioncount] = Vector3D_st( startX, startY + ( sgn_y * i ), static_cast( startZ + ( z_slope_vs_y * i ))); ++collisioncount; } } else if( sgn_y == 0 ) // if we are on the same y-level, just push every x/y coordinate in x-direction from src to trg into the array { for( i = 1; i <= distX; ++i ) { collisions[collisioncount] = Vector3D_st( startX + ( sgn_x * i ), startY, static_cast( startZ + ( z_slope_vs_x * i ))); ++collisioncount; } } else if( distX == distY ) // if we're on a perfect diagonal, we can just go up all coords in both x and y at the same time { for( i = 1; i <= distX; ++i ) { collisions[collisioncount] = Vector3D_st( startX + ( sgn_x * i ), startY + ( sgn_y * i ), static_cast( startZ + ( z_slope_vs_x * i ))); ++collisioncount; } } else { R32 steps = 0; if( distX > distY ) { for( i = 1; i <= distX; ++i ) { collisions[collisioncount] = Vector3D_st( startX + ( sgn_x * i ), startY + ( sgn_y * static_cast( round( static_cast( i ) * static_cast( distY ) / static_cast( distX )))), static_cast( round( startZ + ( z_slope_vs_x * i )))); ++collisioncount; } } else if( distY > distX ) { for( i = 1; i <= distY; ++i ) { collisions[collisioncount] = Vector3D_st( startX + ( sgn_x * static_cast( round( static_cast( i ) * static_cast( distX ) / static_cast( distY )))), startY + ( sgn_y * i ), static_cast( round( startZ + ( z_slope_vs_y * i )))); ++collisioncount; } } } //////////////////////////////////////////////////////// //////////////// This determines which tile-flags to check for UI08 checkthis[ITEM_TYPE_CHOICES]; size_t checkthistotal = 0; UI08 itemtype = 1; while( checkfor ) { if( checkfor >= itemtype && checkfor < ( itemtype * 2 ) && checkfor ) { checkthis[checkthistotal] = itemtype; checkfor = ( checkfor - itemtype ); ++checkthistotal; itemtype = 1; if( checkthistotal == ITEM_TYPE_CHOICES ) break; } else if( checkfor ) { itemtype *= 2; } } std::vector losItemList; UI16 itemCount = 0; // We already have to run through all the collisions in this function, so lets just check and push the ID rather than coming back to it later. for( auto &MapArea : MapRegion->PopulateList( startX, startY, worldNumber )) { if( MapArea == nullptr ) continue; auto regItems = MapArea->GetItemList(); for( const auto &toCheck : regItems->collection() ) { if( !ValidateObject( toCheck ) || toCheck->GetInstanceId() != instanceId ) continue; // If item toCheck is at the exact same spot as the target location, it should not block LoS. if( toCheck->GetX() == destX && toCheck->GetY() == destY && toCheck->GetZ() == destZ ) continue; const UI16 idToPush = DynamicCanBlock( toCheck, collisions, collisioncount, distX, distY, x1, x2, y1, y2, dz ); if( idToPush != INVALIDID ) { auto tile = Tile_st( TileType_t::art ); tile.artInfo= &Map->SeekTile( idToPush ); tile.tileId = idToPush; losItemList.push_back( tile ); ++itemCount; if( itemCount >= LOSXYMAX ) // don't overflow break; } } } auto startMapTile = Map->SeekMap( startX, startY, worldNumber ); if( startMapTile.terrainInfo == nullptr ) return blocked; const bool isStartingUnderground = ( startZ < startMapTile.altitude ); const SI08 terrainLeeway = 3; for( i = 0; i < collisioncount; ++i ) { Vector3D_st& checkLoc = collisions[i]; // Map/Terrain check auto mapTile = Map->SeekMap( checkLoc.x, checkLoc.y, worldNumber ); if( mapTile.terrainInfo == nullptr ) return blocked; const UI16 mapTileId = mapTile.tileId; // debug SI08 mapAltitude = mapTile.altitude; #if defined( UOX_DEBUG_MODE ) if( mChar != nullptr && mChar->IsGM() ) { Console.Print( oldstrutil::format( "LoS Debug at (%u, %u): RayZ=%i, MapZ: %i\n", checkLoc.x, checkLoc.y, static_cast< int >( checkLoc.z ), static_cast< int >( mapAltitude ))); } #endif if( mapTileId != 2 && mapTileId != 475 ) // NoDraw / Cave Entrance { // If ray's current height is lower than ground, LoS has gone underground; blocked! if( !isStartingUnderground && checkLoc.z <= ( mapAltitude - terrainLeeway )) { #if defined( UOX_DEBUG_MODE ) if( mChar != nullptr && mChar->IsGM() ) { Console.Print( "Blocked by terrain\n" ); } Effects->PlayMovingAnimation( mChar, checkLoc.x, checkLoc.y, checkLoc.z, 0x36f4, 3, 0, 1, 0x89d ); #endif return blocked; } } auto artwork = Map->ArtAt( checkLoc.x, checkLoc.y, worldNumber ); for( auto &tile : artwork ) { // Does ray's Z intersect with static item's Z range? if( checkLoc.z >= tile.altitude && checkLoc.z <= ( tile.altitude + tile.height() )) { losItemList.push_back( tile ); } if( losItemList.size() >= LOSXYMAX ) break; } if( losItemList.size() >= LOSXYMAX ) break; } size_t j; for( auto &tile : losItemList ) { for( j = 0; j < checkthistotal; ++j ) { if( CheckFlags( checkthis[j], tile, startZ, destZ, useSurfaceZ )) { return blocked; } } } #if defined( UOX_DEBUG_MODE ) Effects->PlayMovingAnimation( mChar, destX, destY, destZ, 0x36f4, 3, 0, 0, 0x1D3 ); #endif return not_blocked; } //o------------------------------------------------------------------------------------------------o //| Function - CheckItemLineOfSight() //o------------------------------------------------------------------------------------------------o //| Purpose - Checks if an item is within line of sight //o------------------------------------------------------------------------------------------------o bool CheckItemLineOfSight( CChar *mChar, CItem *i ) { if( mChar->IsGM() || mChar->IsCounselor() ) return true; CBaseObject *itemOwner = i; bool inSight = false; if( i->GetCont() != nullptr ) // It's inside another container, we need root container to calculate the lof { ObjectType objType = OT_CBO; CBaseObject *iOwner = FindItemOwner( i, objType ); if( iOwner != nullptr ) { itemOwner = iOwner; } } if( itemOwner == mChar ) { inSight = true; } else { if( mChar->GetInstanceId() != itemOwner->GetInstanceId() ) return false; const SI08 height = Map->TileHeight( itemOwner->GetId() ); // Retrieves actual height of item, unrelated to world-coordinate // Can we see the top or bottom of the item if( LineOfSight( nullptr, mChar, itemOwner->GetX(), itemOwner->GetY(), itemOwner->GetZ(), WALLS_CHIMNEYS + DOORS + FLOORS_FLAT_ROOFING, false )) { inSight = true; } else if( height > 0 ) // Only bother checking for the top of the item if the item has an actual height value, otherwise it's essentially same check twice { if( LineOfSight( nullptr, mChar, itemOwner->GetX(), itemOwner->GetY(), (itemOwner->GetZ() + height), WALLS_CHIMNEYS + DOORS + FLOORS_FLAT_ROOFING, false )) { inSight = true; } } if( inSight == false ) // If both the previous checks failed, try checking from character's Z location to top of item instead { if( LineOfSight( nullptr, mChar, itemOwner->GetX(), itemOwner->GetY(), (itemOwner->GetZ() + height), WALLS_CHIMNEYS + DOORS + FLOORS_FLAT_ROOFING, true )) { inSight = true; } } } return inSight; }