mirror of
https://github.com/UOX3DevTeam/UOX3
synced 2026-08-13 12:27:04 -04:00
Fixed an issue with an internal movement check that prevented characters from moving in valid map areas in Felucca/Trammel from X 6144 to X 7168 Fixed an issue where .instanceID property was misspelled in 'xgo GM command script, causing characters to be teleported to an out of bounds area where they would no longer be saved (js/commands/targeting/x.js) Added some additional error-checking to .Teleport()/.SetLocation() JS Method to prevent script-accidents from sending characters out of bounds. Updated Poisoning skill to add poison-charges to weapons being poisoned. Charges are consumed when applying poison in combat (js/skill/poisoning.js) Updated combat code to consume poison-charges if a poisoned weapon is used to poison an opponent Updated shoplists for Blacksmiths and Weaponsmiths - they now buy/sell longswords (dfndata/items/shoplist.dfn) Updated MageShopping shoplist for mage vendors - they now buy/sell mass curse scrolls (dfndata/items/shoplist.dfn) Added new Spawn Region DFN tag that can specify which era or eras of UO (Multiple comma-separated entries supported) a given spawn region is valid for. Spawn region not valid for core shard era will be ignored. Syntax: ERAS=UO,T2A,UOR,LBR,AOS,SE,ML,SA,HS,TOL Added new Spawn Region DFN tag that allows using another spawn region as a "parent". All properties of this parent will be inherited except for these, which are ignored: ERAS, NPCLIST, ITEMLIST, NPC and ITEM. Syntax for new tag: GET=# // Inherit a specified spawn region Added spawn region for animal trainer vendor in Vesper stable Added spawn region for fur trader/tanner vendors in The Best Hides of Britain Added spawn region for spinner vendor in The Lord's Clothier's and The Right Fit shops in Britain Added spawn region for tanner NPC to Nujel'm Tannery (and fixed leatherworker spawn region for same area) Added spawn regions for banker NPCs in East Bank of Britain, First Bank of Moonglow, Jhelom Bank and Jeweler, Bank of Skara Brae Added spawn regions for stables, tailor and blacksmith inside Castle British Added spawn regions for mage shops, mage guilds and farm houses in Moonglow Added spawn regions for misc vendors and townfolk in multiple cities Added spawn regions for Fire Island Updated spawn regions for Dagger Isle/Ice Island Added spawn regions for forests and jungles in southern Britannia and on various islands including Bucc's Den, Moonglow, Serpent's Hold, and misc unpopulated islands Added spawn regions for area east of Skara Brae, around Hedge Maze and south of Britain Added spawn regions for wandering healers outside every dungeon entrance in Britannia and Lost Lands Re-integrated ocean spawn regions for ocean creatures (dolphin, walrus, water elemental, sea serpent) from the original community-based spawn file Added reagent spawns in overworld spawn regions in Britannia/Lost Lands Added special reagent spawns in swamp areas Added additional overworld spawn regions in Britannia/Lost Lands Added spawn regions for Khaldun dungeon that spawn some new and some old NPCs: Old: Zombies, skeletons, skeletal mages, skeletal knights, ancient liches New: Cursed (and named) NPCs, shadowfiends, zealots of khaldun (knights/summoners), tentacles of the harrower Updated size and positions of some existing overworld spawn regions Fixed incorrect spawn region size for small island south of Trinsic Added switch/door puzzle functionality in Khaldun dungeon using new switch/door combo script (js/item/dungeons/switch_door_combo.js) Added "smart objects" in Khaldun dungeon that activate when you come close enough (js/item/dungeons/smart_activate_item.js) Added teleport locations within Khaldun dungeon (js/teleport.scp) Added custom AIs to existing NPC spawning in Khaldun dungeon: Ancient Lich (summons other undead, and can turn into one of them for disguise) Added DFN entries for new NPCs spawning in Khaldun dungeon, some with custom AIs: [tentacles_of_the_harrower] -> Tentacles of the Harrower (life steal) [shadowfiend] -> Shadowfiends (seek out and reveal hidden players) [zealot_knight]/[zealot_summoner] -> Zealots of Khaldun (turn into undead on death, male/female variants) [cursed] -> Cursed (male/female variants) [spectralarmor] -> Spectral Armor Added DFN entries for new (named) NPCs spawning in Khaldun dungeon: [lysander_gathenwale] -> Lysander Gathenwale [grimmoch_drummel] -> Grimmoch Drummel [morg_bergen] -> Morg Bergen [tavara_sewel] -> Tavara Sewel Added new NPC DFN entries: [leatherworker] -> alias for male/female leatherworker vendors [mapmaker] -> alias for male/female mapmaker vendors Added new Race DFN entries: [RACE 29] -> Cursed (enemies of RACE 30) [RACE 30] -> Zealots (enemies of RACE 29) Added new book DFN entries for Khaldun journals of Lysander, Tavara and Grimmoch - available as loot from the respective NPCs (dfndata/misc/books.dfn and dfndata/items/misc/books.dfn) Added 25% chance for hit-animations and hit-SFX to play for targets in combat, instead of playing every time - can get very spammy Monsters with actual weapons equipped (like Ophidian Enforcer with Halberd/Bardiche) will now get appropriate combat SFX for hitting with that weapon Updated Healing skill to base cure/resurrection skill requirements on the calculated skill, rather than base skill (js/skill/healing.js) Fixed an issue where healing with bandages and dying in the process would not properly reset the healing-related tags and skills used Updated healing script to show a resurrection confirmation menu for players targeted with bandages for resurrect (js/skill/healing.js) All skill usage states now reset on death, to prevent cases of players being "busy" while trying to use skills that got stuck due to script bugs. The 'regspawn GM command now supports a new parameter called "max". Updated command syntax: 'regspawn # // perform a single respawn cycle in a specific spawn region 'regspawn all // perform a single respawn cycle across all spawn regions 'regspawn max // respawn ALL spawn regions to MAX capacity in one go Added new UOX.INI setting that determines the maximum range at which NPCs can initiate attacks on players/NPCs (the old MAXRANGE=10 setting now instead defines max range at which players can initiate attacks on players/NPCs): MAXNPCAGGRORANGE=10 Optimized performance by significantly reducing the number of line-of-sight checks performed by characters, especially in combat scenarios, or when evaluating potential targets for NPC AIs. The amount of checks done is also affected by the new ini setting for max aggro range. Fixed an issue where both NPC and player characters would instantly drop one hunger level (from 6 to 5) upon creation since hungerrate was not initialized until after first hunger-event Updated character priv property from UI16 to UI32, and added a new flag that determines if fame/karma title is hidden for character: HIDEFAMEKARMATITLE (0x10000) NPCs will no longer aggro other NPCs if the Z difference between them is greater or equal to 20 (i.e. they're on different floors), unless both are using ranged weapons and both are in range Fixed an issue where players could get discounts from shopkeepers even though neither player nor shopkeeper were members of a NPC guild (js/npc/ai/shopkeeper.js) Fixed an issue where the "premium" a player would get when selling items to a shopkeeper in the same NPC guild as them would be 110% extra on top of the item's value, instead of the intended 10% (js/npc/ai/shopkeeper.js) (Thanks, cobrag0318!) Added missing tall straw hat to tailoring crafting scripts (Thanks, Dragon Slayer!) Improved error reporting during compilation of individual scripts being reloaded - will now show more context and line number onCombatEnd JS Event now also triggers when NPCs ignore their target and/or evade during combat Updated 'decorate command to support saving/loading custom tags on items for world templates. These are stored after a @ symbol in the world template, each custom tag separated by a | symbol, and with each custom tag saved using this syntax: key$type$value Updated resource-harvesting scripts to use script-specific global (const) variables, to avoid interference between different scripts (js/server/resource/*) Added new resource-harvesting scripts and added spawn regions for these in fields on farms all over Britannia/Lost Lands cabbages (js/server/resource/cabbages.js) canteloupes (js/server/resource/canteloupes.js) carrots (js/server/resource/carrots.js) garlic (js/server/resource/garlic.js) gourds (js/server/resource/gourds.js) honeydew melons (js/server/resource/honeydewmelon.js) onions (js/server/resource/onions.js) pumpkins (js/server/resource/pumpkins.js) squashes (js/server/resource/squashes.js) turnips (js/server/resource/turnips.js) watermelons (js/server/resource/watermelon.js) Updated CSpawnRegion::FindItemSpotToSpawn() function in cSpawnRegion.cpp to prevent duplicate spawning of exact same item in exact same location Updated sectionid of all potion and scroll aliases to match main item, so they'll get correctly picked up by NPC shopkeepers (dfndata/items/magic/potions.dfn and scrolls.dfn) Specified sectionid for backpacks, so it also gets applied to packs added using the alias [backpack] (dfndata/items/misc/provisions.dfn) Fixed a code issue that prevented players from selling empty containers to NPC shopkeepers willing to buy such containers Fixed an issue where field-spells (wall of stone, poison field, etc) would ignore dynamic/static items that should block these Fixed an issue where field-spells could be cast into houses from the outside by standing next to a wall and targeting self Fixed a War/Peace-mode desync between client and server by having server always notify client (using war-toggle packet) when the "at war" flag is toggled for player character Fixed an issue with hiding skill that wouldn't allow player to hide after having acquired self as target in combat (through spellcasting, for instance) (Thanks, Dragon Slayer!) Fixed an issue where mana/reagents were consumed and skillcheck performed before all spellcast-validations had succeeded, resulting in the occasional loss of reagents/mana when spellcasting was disallowed (code and js/magic/clumsy.js and level1targ.js) Fixed an issue with lack of criminal-flagging for casting of hostile spells scripted in JS (clumsy, magic arrow, feeblemind) vs blue targets (js/magic/clumsy.js and level1targ.js) Fixed some issues with Line-of-Sight checking code that returned incorrect results, which amongst other things affected spellcasting in areas with uneven terrain Fixed some issues with movement code and climbing of ladders, in particular the rope ladder in the 2-story log cabin. Code now sets a "is climbing" flag when character steps onto a rope ladder, and uses that to help resolve some edge cases, and then unsets it when they step off. Renamed section headers of some map-items in DFNs from "locationname" to "locationnamemap" for clarity (example: "britain" to "britainmap") Fixed broken "map wrapping" when sailing to edges of map in Felucca or Trammel Fixed a server crash related to attempts at reading data from invalid map tiles
799 lines
25 KiB
C++
799 lines
25 KiB
C++
#include "uox3.h"
|
|
#include "regions.h"
|
|
#include "cEffects.h"
|
|
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
|
|
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<R32>( dir.z );
|
|
}
|
|
return static_cast<R32>( dir.z ) / static_cast<R32>( dir.x );
|
|
}
|
|
|
|
//==================================================================================================
|
|
inline auto Line3D_st::dzInDirectionY() const ->R32
|
|
{
|
|
if( dir.y == 0 )
|
|
{
|
|
return static_cast<R32>( dir.z );
|
|
}
|
|
return static_cast<R32>( dir.z ) / static_cast<R32>( 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<R32>( loc.x ), static_cast<R32>( loc.y )), Vector2D_st( static_cast<R32>( dir.x ), static_cast<R32>( 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<UI16>( 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<SI32>( destX - startX) ), distY = abs( static_cast<SI32>( destY - startY ));
|
|
const SI32 distZ = destZ - startZ; // abs( static_cast<SI32>( destZ - startZ ));
|
|
|
|
Line3D_st lineofsight = Line3D_st( Vector3D_st( startX, startY, startZ ), Vector3D_st( distX, distY, distZ ));
|
|
|
|
const R64 rBlah = ( static_cast<R64>( distX ) * static_cast<R64>( distX )) + ( static_cast<R64>( distY ) * static_cast<R64>( distY ));
|
|
const SI32 distance = static_cast<SI32>( 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<Vector3D_st> 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<R32>( distZ ) / static_cast<R32>( distY );
|
|
}
|
|
R32 z_slope_vs_x = 0.0f;
|
|
if( distX != 0 )
|
|
{
|
|
z_slope_vs_x = static_cast<R32>( distZ ) / static_cast<R32>( distX );
|
|
}
|
|
|
|
SI32 dz = 0;
|
|
if( distX > distY )
|
|
{
|
|
dz = static_cast<SI32>( floor( abs( z_slope_vs_x )));
|
|
}
|
|
else
|
|
{
|
|
dz = static_cast<SI32>( 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<SI08>(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<SI08>( 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<SI08>( 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<SI08>( 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<R32>( round( static_cast<R32>( i ) * static_cast<R32>( distY ) / static_cast<R32>( distX )))), static_cast<SI08>( 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<R32>( round( static_cast<R32>( i ) * static_cast<R32>( distX ) / static_cast<R32>( distY )))), startY + ( sgn_y * i ), static_cast<SI08>( 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<Tile_st> 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;
|
|
}
|