using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using System.Text;
using log4net;
using ACE.Common;
using ACE.Common.Extensions;
using ACE.Database;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Entity.Models;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Managers;
using ACE.Server.Network;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Network.Sequence;
using ACE.Server.Network.Structure;
using ACE.Server.Physics;
using ACE.Server.Physics.Animation;
using ACE.Server.Physics.Common;
using ACE.Server.Physics.Util;
using ACE.Server.WorldObjects.Managers;
using Landblock = ACE.Server.Entity.Landblock;
using Position = ACE.Entity.Position;
namespace ACE.Server.WorldObjects
{
///
/// Base Object for Game World
///
public abstract partial class WorldObject : IActor
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
///
/// If this object was created from a weenie (and not a database biota), this is the source.
/// You should never manipulate these values. You should only reference these values in extreme cases.
///
public Weenie Weenie { get; }
///
/// This is object property overrides that should have come from the shard db (or init to defaults of object is new to this instance).
/// You should not manipulate these values directly. To manipulate this use the exposed SetProperty and RemoveProperty functions instead.
///
public Biota Biota { get; }
///
/// This is just a wrapper around Biota.Id
///
public ObjectGuid Guid { get; }
public PhysicsObj PhysicsObj { get; protected set; }
public ObjectDescriptionFlag ObjectDescriptionFlags { get; protected set; }
public SequenceManager Sequences { get; } = new SequenceManager();
public virtual float ListeningRadius { get; protected set; } = 5f;
///
/// Should only be adjusted by Landblock -- default is null
///
public Landblock CurrentLandblock { get; internal set; }
public bool IsBusy { get; set; }
public bool IsShield { get => CombatUse != null && CombatUse == ACE.Entity.Enum.CombatUse.Shield; }
// ValidLocations is bugged for some older two-handed weapons, still contains MeleeWeapon instead of TwoHanded?
//public bool IsTwoHanded { get => CurrentWieldedLocation != null && CurrentWieldedLocation == EquipMask.TwoHanded; }
public bool IsTwoHanded => WeaponSkill == Skill.TwoHandedCombat;
public bool IsBow { get => DefaultCombatStyle != null && (DefaultCombatStyle == CombatStyle.Bow || DefaultCombatStyle == CombatStyle.Crossbow); }
public bool IsAtlatl { get => DefaultCombatStyle != null && DefaultCombatStyle == CombatStyle.Atlatl; }
public bool IsAmmoLauncher { get => IsBow || IsAtlatl; }
public bool IsThrownWeapon { get => DefaultCombatStyle != null && DefaultCombatStyle == CombatStyle.ThrownWeapon; }
public bool IsRanged { get => IsAmmoLauncher || IsThrownWeapon; }
public bool IsCaster { get => DefaultCombatStyle != null && (DefaultCombatStyle == CombatStyle.Magic); }
public EmoteManager EmoteManager;
public EnchantmentManagerWithCaching EnchantmentManager;
// todo: move these to a base projectile class
public WorldObject ProjectileSource { get; set; }
public WorldObject ProjectileTarget { get; set; }
public WorldObject ProjectileLauncher { get; set; }
public WorldObject ProjectileAmmo { get; set; }
public bool HitMsg; // FIXME: find a better way to do this for projectiles
public WorldObject Wielder;
public WorldObject() { }
///
/// A new biota will be created taking all of its values from weenie.
///
protected WorldObject(Weenie weenie, ObjectGuid guid)
{
Weenie = weenie;
Biota = ACE.Entity.Adapter.WeenieConverter.ConvertToBiota(weenie, guid.Full, false, true);
Guid = guid;
InitializePropertyDictionaries();
SetEphemeralValues();
InitializeGenerator();
InitializeHeartbeats();
CreationTimestamp = (int)Time.GetUnixTime();
}
///
/// Restore a WorldObject from the database.
/// Any properties tagged as Ephemeral will be removed from the biota.
///
protected WorldObject(Biota biota)
{
Biota = biota;
Guid = new ObjectGuid(Biota.Id);
biotaOriginatedFromDatabase = true;
InitializePropertyDictionaries();
SetEphemeralValues();
InitializeGenerator();
InitializeHeartbeats();
}
public bool BumpVelocity { get; set; }
///
/// Initializes a new default physics object
///
public virtual void InitPhysicsObj()
{
//Console.WriteLine($"InitPhysicsObj({Name} - {Guid})");
var defaultState = CalculatedPhysicsState();
if (!(this is Creature))
{
var isDynamic = Static == null || !Static.Value;
var setupTableId = SetupTableId;
// TODO: REMOVE ME?
// Temporary workaround fix to account for ace spawn placement issues with certain hooked objects.
if (this is Hook)
{
var hookWeenie = DatabaseManager.World.GetCachedWeenie(WeenieClassId);
setupTableId = hookWeenie.GetProperty(PropertyDataId.Setup) ?? SetupTableId;
}
// TODO: REMOVE ME?
PhysicsObj = PhysicsObj.makeObject(setupTableId, Guid.Full, isDynamic);
}
else
{
PhysicsObj = new PhysicsObj();
PhysicsObj.makeAnimObject(SetupTableId, true);
}
PhysicsObj.set_object_guid(Guid);
PhysicsObj.set_weenie_obj(new WeenieObject(this));
PhysicsObj.SetMotionTableID(MotionTableId);
PhysicsObj.SetScaleStatic(ObjScale ?? 1.0f);
PhysicsObj.State = defaultState;
//if (creature != null) AllowEdgeSlide = true;
if (BumpVelocity)
PhysicsObj.Velocity = new Vector3(0, 0, 0.5f);
}
public bool AddPhysicsObj()
{
if (PhysicsObj.CurCell != null)
return false;
AdjustDungeon(Location);
// exclude linkspots from spawning
if (WeenieClassId == 10762) return true;
var cell = LScape.get_landcell(Location.Cell);
if (cell == null)
{
PhysicsObj.DestroyObject();
PhysicsObj = null;
return false;
}
PhysicsObj.Position.ObjCellID = cell.ID;
var location = new Physics.Common.Position();
location.ObjCellID = cell.ID;
location.Frame.Origin = Location.Pos;
location.Frame.Orientation = Location.Rotation;
var success = PhysicsObj.enter_world(location);
if (!success || PhysicsObj.CurCell == null)
{
PhysicsObj.DestroyObject();
PhysicsObj = null;
//Console.WriteLine($"AddPhysicsObj: failure: {Name} @ {cell.ID.ToString("X8")} - {Location.Pos} - {Location.Rotation} - SetupID: {SetupTableId.ToString("X8")}, MTableID: {MotionTableId.ToString("X8")}");
return false;
}
//Console.WriteLine($"AddPhysicsObj: success: {Name} ({Guid})");
SyncLocation();
SetPosition(PositionType.Home, new Position(Location));
return true;
}
public void SyncLocation()
{
Location.LandblockId = new LandblockId(PhysicsObj.Position.ObjCellID);
// skip ObjCellID check when updating from physics
// TODO: update to newer version of ACE.Entity.Position
Location.PositionX = PhysicsObj.Position.Frame.Origin.X;
Location.PositionY = PhysicsObj.Position.Frame.Origin.Y;
Location.PositionZ = PhysicsObj.Position.Frame.Origin.Z;
Location.Rotation = PhysicsObj.Position.Frame.Orientation;
}
private void InitializePropertyDictionaries()
{
if (Biota.PropertiesEnchantmentRegistry == null)
Biota.PropertiesEnchantmentRegistry = new Collection();
}
private void SetEphemeralValues()
{
ObjectDescriptionFlags = ObjectDescriptionFlag.Attackable;
EmoteManager = new EmoteManager(this);
EnchantmentManager = new EnchantmentManagerWithCaching(this);
if (Placement == null)
Placement = ACE.Entity.Enum.Placement.Resting;
if (MotionTableId != 0)
CurrentMotionState = new Motion(MotionStance.Invalid);
}
///
/// This will be true when teleporting
///
public bool Teleporting { get; set; } = false;
public bool HasGiveOrRefuseEmoteForItem(WorldObject item, out PropertiesEmote emote)
{
// NPC refuses this item, with a custom response
var refuseItem = EmoteManager.GetEmoteSet(EmoteCategory.Refuse, null, null, item.WeenieClassId);
if (refuseItem != null)
{
emote = refuseItem;
return true;
}
// NPC accepts this item
var giveItem = EmoteManager.GetEmoteSet(EmoteCategory.Give, null, null, item.WeenieClassId);
if (giveItem != null)
{
emote = giveItem;
return true;
}
emote = null;
return false;
}
///
/// Returns TRUE if this object has wo in VisibleTargets list
///
public bool IsVisibleTarget(WorldObject wo)
{
if (PhysicsObj == null || wo.PhysicsObj == null)
return false;
// note: VisibleTargets is only maintained for monsters and combat pets
return PhysicsObj.ObjMaint.VisibleTargetsContainsKey(wo.PhysicsObj.ID);
}
//public static PhysicsObj SightObj = PhysicsObj.makeObject(0x02000124, 0, false, true); // arrow
///
/// Returns TRUE if this object has direct line-of-sight visibility to input object
///
public bool IsDirectVisible(WorldObject wo)
{
if (PhysicsObj == null || wo.PhysicsObj == null)
return false;
var SightObj = PhysicsObj.makeObject(0x02000124, 0, false, true);
SightObj.State |= PhysicsState.Missile;
var startPos = new Physics.Common.Position(PhysicsObj.Position);
var targetPos = new Physics.Common.Position(wo.PhysicsObj.Position);
if (PhysicsObj.GetBlockDist(startPos, targetPos) > 1)
return false;
// set to eye level
startPos.Frame.Origin.Z += PhysicsObj.GetHeight() - SightObj.GetHeight();
targetPos.Frame.Origin.Z += wo.PhysicsObj.GetHeight() - SightObj.GetHeight();
var dir = Vector3.Normalize(targetPos.Frame.Origin - startPos.Frame.Origin);
var radsum = PhysicsObj.GetPhysicsRadius() + SightObj.GetPhysicsRadius();
startPos.Frame.Origin += dir * radsum;
SightObj.CurCell = PhysicsObj.CurCell;
SightObj.ProjectileTarget = wo.PhysicsObj;
// perform line of sight test
var transition = SightObj.transition(startPos, targetPos, false);
SightObj.DestroyObject();
if (transition == null) return false;
// check if target object was reached
var isVisible = transition.CollisionInfo.CollideObject.FirstOrDefault(c => c.ID == wo.PhysicsObj.ID) != null;
return isVisible;
}
public bool IsDirectVisible(Position pos)
{
if (PhysicsObj == null)
return false;
var SightObj = PhysicsObj.makeObject(0x02000124, 0, false, true);
SightObj.State |= PhysicsState.Missile;
var startPos = new Physics.Common.Position(PhysicsObj.Position);
var targetPos = new Physics.Common.Position(pos);
if (PhysicsObj.GetBlockDist(startPos, targetPos) > 1)
return false;
// set to eye level
startPos.Frame.Origin.Z += PhysicsObj.GetHeight() - SightObj.GetHeight();
targetPos.Frame.Origin.Z += SightObj.GetHeight();
var dir = Vector3.Normalize(targetPos.Frame.Origin - startPos.Frame.Origin);
var radsum = PhysicsObj.GetPhysicsRadius() + SightObj.GetPhysicsRadius();
startPos.Frame.Origin += dir * radsum;
SightObj.CurCell = PhysicsObj.CurCell;
SightObj.ProjectileTarget = PhysicsObj;
// perform line of sight test
var transition = SightObj.transition(targetPos, startPos, false);
SightObj.DestroyObject();
if (transition == null) return false;
// check if target object was reached
var isVisible = transition.CollisionInfo.CollideObject.FirstOrDefault(c => c.ID == PhysicsObj.ID) != null;
return isVisible;
}
public bool IsMeleeVisible(WorldObject wo)
{
if (PhysicsObj == null || wo.PhysicsObj == null)
return false;
var startPos = new Physics.Common.Position(PhysicsObj.Position);
var targetPos = new Physics.Common.Position(wo.PhysicsObj.Position);
PhysicsObj.ProjectileTarget = wo.PhysicsObj;
// perform line of sight test
var transition = PhysicsObj.transition(startPos, targetPos, false);
PhysicsObj.ProjectileTarget = null;
if (transition == null) return false;
// check if target object was reached
var isVisible = transition.CollisionInfo.CollideObject.FirstOrDefault(c => c.ID == wo.PhysicsObj.ID) != null;
return isVisible;
}
public bool IsProjectileVisible(WorldObject proj)
{
if (!(this is Creature) || (Ethereal ?? false))
return true;
if (PhysicsObj == null || proj.PhysicsObj == null)
return false;
var startPos = new Physics.Common.Position(proj.PhysicsObj.Position);
var targetPos = new Physics.Common.Position(PhysicsObj.Position);
// set to eye level
targetPos.Frame.Origin.Z += PhysicsObj.GetHeight() - proj.PhysicsObj.GetHeight();
var prevTarget = proj.PhysicsObj.ProjectileTarget;
proj.PhysicsObj.ProjectileTarget = PhysicsObj;
// perform line of sight test
var transition = proj.PhysicsObj.transition(startPos, targetPos, false);
proj.PhysicsObj.ProjectileTarget = prevTarget;
if (transition == null) return false;
// check if target object was reached
var isVisible = transition.CollisionInfo.CollideObject.FirstOrDefault(c => c.ID == PhysicsObj.ID) != null;
return isVisible;
}
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
public MoveToState LastMoveToState { get; set; }
public Position RequestedLocation { get; set; }
///
/// Flag indicates if RequestedLocation should be broadcast to other players
/// - For AutoPos packets, this is set to TRUE
/// - For MoveToState packets, this is set to FALSE
///
public bool RequestedLocationBroadcast { get; set; }
////// Logical Game Data
public ContainerType ContainerType
{
get
{
if (WeenieType == WeenieType.Container)
return ContainerType.Container;
else if (RequiresPackSlot)
return ContainerType.Foci;
else
return ContainerType.NonContainer;
}
}
public string DebugOutputString(WorldObject obj)
{
var sb = new StringBuilder();
sb.AppendLine("ACE Debug Output:");
sb.AppendLine("ACE Class File: " + GetType().Name + ".cs");
sb.AppendLine("Guid: " + obj.Guid.Full + " (0x" + obj.Guid.Full.ToString("X") + ")");
sb.AppendLine("----- Private Fields -----");
foreach (var prop in obj.GetType().GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).OrderBy(field => field.Name))
{
if (prop.GetValue(obj) == null)
continue;
sb.AppendLine($"{prop.Name.Replace("<", "").Replace(">k__BackingField", "")} = {prop.GetValue(obj)}");
}
sb.AppendLine("----- Public Properties -----");
foreach (var prop in obj.GetType().GetProperties().OrderBy(property => property.Name))
{
if (prop.GetValue(obj, null) == null)
continue;
switch (prop.Name.ToLower())
{
case "guid":
sb.AppendLine($"{prop.Name} = {obj.Guid.Full} (GuidType.{obj.Guid.Type.ToString()})");
break;
case "descriptionflags":
sb.AppendLine($"{prop.Name} = {ObjectDescriptionFlags.ToString()}" + " (" + (uint)ObjectDescriptionFlags + ")");
break;
case "weenieflags":
var weenieFlags = CalculateWeenieHeaderFlag();
sb.AppendLine($"{prop.Name} = {weenieFlags.ToString()}" + " (" + (uint)weenieFlags + ")");
break;
case "weenieflags2":
var weenieFlags2 = CalculateWeenieHeaderFlag2();
sb.AppendLine($"{prop.Name} = {weenieFlags2.ToString()}" + " (" + (uint)weenieFlags2 + ")");
break;
case "itemtype":
sb.AppendLine($"{prop.Name} = {obj.ItemType.ToString()}" + " (" + (uint)obj.ItemType + ")");
break;
case "creaturetype":
sb.AppendLine($"{prop.Name} = {obj.CreatureType.ToString()}" + " (" + (uint)obj.CreatureType + ")");
break;
case "containertype":
sb.AppendLine($"{prop.Name} = {obj.ContainerType.ToString()}" + " (" + (uint)obj.ContainerType + ")");
break;
case "usable":
sb.AppendLine($"{prop.Name} = {obj.ItemUseable.ToString()}" + " (" + (uint)obj.ItemUseable + ")");
break;
case "radarbehavior":
sb.AppendLine($"{prop.Name} = {obj.RadarBehavior.ToString()}" + " (" + (uint)obj.RadarBehavior + ")");
break;
case "physicsdescriptionflag":
var physicsDescriptionFlag = CalculatedPhysicsDescriptionFlag();
sb.AppendLine($"{prop.Name} = {physicsDescriptionFlag.ToString()}" + " (" + (uint)physicsDescriptionFlag + ")");
break;
case "physicsstate":
var physicsState = PhysicsObj.State;
sb.AppendLine($"{prop.Name} = {physicsState.ToString()}" + " (" + (uint)physicsState + ")");
break;
//case "propertiesspellid":
// foreach (var item in obj.PropertiesSpellId)
// {
// sb.AppendLine($"PropertySpellId.{Enum.GetName(typeof(Spell), item.SpellId)} ({item.SpellId})");
// }
// break;
case "validlocations":
sb.AppendLine($"{prop.Name} = {obj.ValidLocations}" + " (" + (uint)obj.ValidLocations + ")");
break;
case "currentwieldedlocation":
sb.AppendLine($"{prop.Name} = {obj.CurrentWieldedLocation}" + " (" + (uint)obj.CurrentWieldedLocation + ")");
break;
case "priority":
sb.AppendLine($"{prop.Name} = {obj.ClothingPriority}" + " (" + (uint)obj.ClothingPriority + ")");
break;
case "radarcolor":
sb.AppendLine($"{prop.Name} = {obj.RadarColor}" + " (" + (uint)obj.RadarColor + ")");
break;
case "location":
sb.AppendLine($"{prop.Name} = {obj.Location.ToLOCString()}");
break;
case "destination":
sb.AppendLine($"{prop.Name} = {obj.Destination.ToLOCString()}");
break;
case "instantiation":
sb.AppendLine($"{prop.Name} = {obj.Instantiation.ToLOCString()}");
break;
case "sanctuary":
sb.AppendLine($"{prop.Name} = {obj.Sanctuary.ToLOCString()}");
break;
case "home":
sb.AppendLine($"{prop.Name} = {obj.Home.ToLOCString()}");
break;
case "portalsummonloc":
sb.AppendLine($"{prop.Name} = {obj.PortalSummonLoc.ToLOCString()}");
break;
case "houseboot":
sb.AppendLine($"{prop.Name} = {obj.HouseBoot.ToLOCString()}");
break;
case "lastoutsidedeath":
sb.AppendLine($"{prop.Name} = {obj.LastOutsideDeath.ToLOCString()}");
break;
case "linkedlifestone":
sb.AppendLine($"{prop.Name} = {obj.LinkedLifestone.ToLOCString()}");
break;
case "channelsactive":
sb.AppendLine($"{prop.Name} = {(Channel)obj.GetProperty(PropertyInt.ChannelsActive)}" + " (" + (uint)obj.GetProperty(PropertyInt.ChannelsActive) + ")");
break;
case "channelsallowed":
sb.AppendLine($"{prop.Name} = {(Channel)obj.GetProperty(PropertyInt.ChannelsAllowed)}" + " (" + (uint)obj.GetProperty(PropertyInt.ChannelsAllowed) + ")");
break;
case "playerkillerstatus":
sb.AppendLine($"{prop.Name} = {obj.PlayerKillerStatus}" + " (" + (uint)obj.PlayerKillerStatus + ")");
break;
default:
sb.AppendLine($"{prop.Name} = {prop.GetValue(obj, null)}");
break;
}
}
sb.AppendLine("----- Property Dictionaries -----");
foreach (var item in obj.GetAllPropertyBools())
sb.AppendLine($"PropertyBool.{Enum.GetName(typeof(PropertyBool), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyDataId())
sb.AppendLine($"PropertyDataId.{Enum.GetName(typeof(PropertyDataId), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyFloat())
sb.AppendLine($"PropertyFloat.{Enum.GetName(typeof(PropertyFloat), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyInstanceId())
sb.AppendLine($"PropertyInstanceId.{Enum.GetName(typeof(PropertyInstanceId), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyInt())
sb.AppendLine($"PropertyInt.{Enum.GetName(typeof(PropertyInt), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyInt64())
sb.AppendLine($"PropertyInt64.{Enum.GetName(typeof(PropertyInt64), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyString())
sb.AppendLine($"PropertyString.{Enum.GetName(typeof(PropertyString), item.Key)} ({(int)item.Key}) = {item.Value}");
sb.AppendLine("\n");
return sb.ToString().Replace("\r", "");
}
public void QueryHealth(Session examiner)
{
float healthPercentage = 1f;
if (this is Creature creature)
healthPercentage = (float)creature.Health.Current / creature.Health.MaxValue;
var updateHealth = new GameEventUpdateHealth(examiner, Guid.Full, healthPercentage);
examiner.Network.EnqueueSend(updateHealth);
}
public void QueryItemMana(Session examiner)
{
float manaPercentage = 1f;
uint success = 0;
if (ItemCurMana != null && ItemMaxMana != null)
{
manaPercentage = (float)ItemCurMana / (float)ItemMaxMana;
success = 1;
}
if (success == 0) // according to retail PCAPs, if success = 0, mana = 0.
manaPercentage = 0;
var updateMana = new GameEventQueryItemManaResponse(examiner, Guid.Full, manaPercentage, success);
examiner.Network.EnqueueSend(updateMana);
}
public void EnqueueBroadcastPhysicsState()
{
if (PhysicsObj != null)
{
if (!Visibility)
EnqueueBroadcast(new GameMessageSetState(this, PhysicsObj.State));
else
{
if (this is Player player && player.CloakStatus == CloakStatus.On)
{
var ps = PhysicsObj.State;
ps &= ~PhysicsState.Cloaked;
ps &= ~PhysicsState.NoDraw;
player.Session.Network.EnqueueSend(new GameMessageSetState(this, PhysicsObj.State));
EnqueueBroadcast(false, new GameMessageSetState(this, ps));
}
else
EnqueueBroadcast(new GameMessageSetState(this, PhysicsObj.State));
}
}
}
public void EnqueueBroadcastUpdateObject()
{
EnqueueBroadcast(new GameMessageUpdateObject(this));
}
public virtual void OnCollideObject(WorldObject target)
{
// thrown weapons
if (ProjectileTarget == null) return;
ProjectileCollisionHelper.OnCollideObject(this, target);
}
public virtual void OnCollideObjectEnd(WorldObject target)
{
// empty base
}
public virtual void OnCollideEnvironment()
{
// thrown weapons
if (ProjectileTarget == null) return;
ProjectileCollisionHelper.OnCollideEnvironment(this);
}
public void ApplyVisualEffects(PlayScript effect, float speed = 1)
{
if (CurrentLandblock != null)
PlayParticleEffect(effect, Guid, speed);
}
// plays particle effect like spell casting or bleed etc..
public void PlayParticleEffect(PlayScript effectId, ObjectGuid targetId, float speed = 1)
{
EnqueueBroadcast(new GameMessageScript(targetId, effectId, speed));
}
public void ApplySoundEffects(Sound sound, float volume = 1)
{
if (CurrentLandblock != null)
PlaySoundEffect(sound, Guid, volume);
}
public void PlaySoundEffect(Sound soundId, ObjectGuid targetId, float volume = 1)
{
EnqueueBroadcast(new GameMessageSound(targetId, soundId, volume));
}
public virtual void OnGeneration(WorldObject generator)
{
//Console.WriteLine($"{Name}.OnGeneration()");
EmoteManager.OnGeneration();
}
public virtual bool EnterWorld()
{
if (Location == null)
return false;
if (!LandblockManager.AddObject(this))
return false;
if (SuppressGenerateEffect != true)
ApplyVisualEffects(PlayScript.Create);
if (Generator != null)
OnGeneration(Generator);
//Console.WriteLine($"{Name}.EnterWorld()");
return true;
}
// todo: This should really be an extension method for Position, or a static method within Position or even AdjustPos
public static void AdjustDungeon(Position pos)
{
AdjustDungeonPos(pos);
AdjustDungeonCells(pos);
}
// todo: This should really be an extension method for Position, or a static method within Position or even AdjustPos
public static bool AdjustDungeonCells(Position pos)
{
if (pos == null) return false;
var landblock = LScape.get_landblock(pos.Cell);
if (landblock == null || !landblock.HasDungeon) return false;
var dungeonID = pos.Cell >> 16;
var adjustCell = AdjustCell.Get(dungeonID);
var cellID = adjustCell.GetCell(pos.Pos);
if (cellID != null && pos.Cell != cellID.Value)
{
pos.LandblockId = new LandblockId(cellID.Value);
return true;
}
return false;
}
// todo: This should really be an extension method for Position, or a static method within Position, or even AdjustPos
public static bool AdjustDungeonPos(Position pos)
{
if (pos == null) return false;
var landblock = LScape.get_landblock(pos.Cell);
if (landblock == null || !landblock.HasDungeon) return false;
var dungeonID = pos.Cell >> 16;
var adjusted = AdjustPos.Adjust(dungeonID, pos);
return adjusted;
}
///
/// Returns a strike message based on damage type and severity
///
public virtual string GetAttackMessage(Creature creature, DamageType damageType, uint amount)
{
var percent = (float)amount / creature.Health.Base;
string verb = null, plural = null;
Strings.GetAttackVerb(damageType, percent, ref verb, ref plural);
var type = damageType.GetName().ToLower();
return $"You {verb} {creature.Name} for {amount} points of {type} damage!";
}
public Dictionary GetProperties(WorldObject wo)
{
var props = new Dictionary();
var fields = Enum.GetValues(typeof(PropertyInt)).Cast();
foreach (var field in fields)
{
var prop = wo.GetProperty(field);
props.Add(field, prop);
}
return props;
}
///
/// Returns the base damage for a weapon
///
public virtual BaseDamage GetBaseDamage()
{
var maxDamage = GetProperty(PropertyInt.Damage) ?? 0;
var variance = GetProperty(PropertyFloat.DamageVariance) ?? 0;
return new BaseDamage(maxDamage, (float)variance);
}
///
/// Returns the modified damage for a weapon,
/// with the wielder enchantments taken into account
///
public BaseDamageMod GetDamageMod(Creature wielder, WorldObject weapon = null)
{
var baseDamage = GetBaseDamage();
if (weapon == null)
weapon = wielder.GetEquippedWeapon();
var baseDamageMod = new BaseDamageMod(baseDamage, wielder, weapon);
return baseDamageMod;
}
public bool IsDestroyed { get; private set; }
///
/// If this is a container or a creature, all of the inventory and/or equipped objects will also be destroyed.
/// An object should only be destroyed once.
///
public void Destroy(bool raiseNotifyOfDestructionEvent = true, bool fromLandblockUnload = false)
{
if (IsDestroyed)
{
//log.WarnFormat("Item 0x{0:X8}:{1} called destroy more than once.", Guid.Full, Name);
return;
}
IsDestroyed = true;
ReleasedTimestamp = Time.GetUnixTime();
if (this is Container container)
{
foreach (var item in container.Inventory.Values)
item.Destroy();
}
if (this is Creature creature)
{
foreach (var item in creature.EquippedObjects.Values)
item.Destroy();
}
if (this is Pet pet)
{
if (pet.P_PetOwner?.CurrentActivePet == this)
pet.P_PetOwner.CurrentActivePet = null;
if (pet.P_PetDevice?.Pet == Guid.Full)
pet.P_PetDevice.Pet = null;
}
if (this is Vendor vendor)
{
foreach (var wo in vendor.DefaultItemsForSale.Values)
wo.Destroy();
foreach (var wo in vendor.UniqueItemsForSale.Values)
wo.Destroy();
}
if (raiseNotifyOfDestructionEvent)
NotifyOfEvent(RegenerationType.Destruction);
if (IsGenerator)
{
if (fromLandblockUnload)
ProcessGeneratorDestructionDirective(GeneratorDestruct.Destroy, fromLandblockUnload);
else
OnGeneratorDestroy();
}
CurrentLandblock?.RemoveWorldObject(Guid);
RemoveBiotaFromDatabase();
if (Guid.IsDynamic())
GuidManager.RecycleDynamicGuid(Guid);
}
public void FadeOutAndDestroy(bool raiseNotifyOfDestructionEvent = true)
{
EnqueueBroadcast(new GameMessageScript(Guid, PlayScript.Destroy));
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(1.0f);
actionChain.AddAction(this, () => Destroy(raiseNotifyOfDestructionEvent));
actionChain.EnqueueChain();
}
public string GetPluralName()
{
var pluralName = PluralName;
if (pluralName == null)
pluralName = Name.Pluralize();
return pluralName;
}
///
/// Returns TRUE if this object has non-cyclic animations in progress
///
public bool IsAnimating { get => PhysicsObj != null && PhysicsObj.IsAnimating; }
///
/// Executes a motion/animation for this object
/// adds to the physics animation system, and broadcasts to nearby players
///
/// The amount it takes to execute the motion
public float ExecuteMotion(Motion motion, bool sendClient = true, float? maxRange = null, bool persist = false)
{
var motionCommand = motion.MotionState.ForwardCommand;
if (motionCommand == MotionCommand.Ready)
motionCommand = (MotionCommand)motion.Stance;
// run motion command on server through physics animation system
if (PhysicsObj != null && motionCommand != MotionCommand.Ready)
{
var motionInterp = PhysicsObj.get_minterp();
var rawState = new Physics.Animation.RawMotionState();
rawState.ForwardCommand = 0; // always 0? must be this for monster sleep animations (skeletons, golems)
// else the monster will immediately wake back up..
rawState.CurrentHoldKey = HoldKey.Run;
rawState.CurrentStyle = (uint)motionCommand;
if (!PhysicsObj.IsMovingOrAnimating)
//PhysicsObj.UpdateTime = PhysicsTimer.CurrentTime - PhysicsGlobals.MinQuantum;
PhysicsObj.UpdateTime = PhysicsTimer.CurrentTime;
motionInterp.RawState = rawState;
motionInterp.apply_raw_movement(true, true);
}
if (persist && PropertyManager.GetBool("persist_movement").Item)
motion.Persist(CurrentMotionState);
// hardcoded ready?
var animLength = MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, CurrentMotionState.MotionState.ForwardCommand, motionCommand);
CurrentMotionState = motion;
// broadcast to nearby players
if (sendClient)
EnqueueBroadcastMotion(motion, maxRange, false);
return animLength;
}
public float ExecuteMotionPersist(Motion motion, bool sendClient = true, float? maxRange = null)
{
return ExecuteMotion(motion, sendClient, maxRange, true);
}
public void SetStance(MotionStance stance, bool broadcast = true)
{
var motion = new Motion(stance);
if (PropertyManager.GetBool("persist_movement").Item)
motion.Persist(CurrentMotionState);
CurrentMotionState = motion;
if (broadcast)
EnqueueBroadcastMotion(CurrentMotionState);
}
///
/// Returns the relative direction of this creature in relation to target
/// expressed as a quadrant: Front/Back, Left/Right
///
public Quadrant GetRelativeDir(WorldObject target)
{
var sourcePos = new Vector3(Location.PositionX, Location.PositionY, 0);
var targetPos = new Vector3(target.Location.PositionX, target.Location.PositionY, 0);
var targetDir = new AFrame(target.Location.Pos, target.Location.Rotation).get_vector_heading();
targetDir.Z = 0;
targetDir = Vector3.Normalize(targetDir);
var sourceToTarget = Vector3.Normalize(sourcePos - targetPos);
var dir = Vector3.Dot(sourceToTarget, targetDir);
var angle = Vector3.Cross(sourceToTarget, targetDir);
var quadrant = angle.Z <= 0 ? Quadrant.Left : Quadrant.Right;
quadrant |= dir >= 0 ? Quadrant.Front : Quadrant.Back;
return quadrant;
}
///
/// Returns TRUE if this WorldObject is a generic linkspot
/// Linkspots are used for things like Houses,
/// where the portal destination should be populated at runtime.
///
public bool IsLinkSpot => WeenieType == WeenieType.Generic && WeenieClassName.Equals("portaldestination");
public const float LocalBroadcastRange = 96.0f;
public const float LocalBroadcastRangeSq = LocalBroadcastRange * LocalBroadcastRange;
public SetPosition ScatterPos { get; set; }
public DestinationType DestinationType;
public Skill ConvertToMoASkill(Skill skill)
{
if (this is Player player)
{
if (SkillExtensions.RetiredMelee.Contains(skill))
return player.GetHighestMeleeSkill();
if (SkillExtensions.RetiredMissile.Contains(skill))
return Skill.MissileWeapons;
}
return skill;
}
public void GetCurrentMotionState(out MotionStance currentStance, out MotionCommand currentMotion)
{
currentStance = MotionStance.Invalid;
currentMotion = MotionCommand.Ready;
if (CurrentMotionState != null)
{
currentStance = CurrentMotionState.Stance;
if (CurrentMotionState.MotionState != null)
currentMotion = CurrentMotionState.MotionState.ForwardCommand;
}
}
public virtual void HandleMotionDone(uint motionID, bool success)
{
// empty base
}
public virtual void OnMoveComplete(WeenieError status)
{
// empty base
}
public bool IsTradeNote => ItemType == ItemType.PromissoryNote;
public virtual bool IsAttunedOrContainsAttuned => Attuned >= AttunedStatus.Attuned;
public virtual bool IsStickyAttunedOrContainsStickyAttuned => Attuned >= AttunedStatus.Sticky;
public virtual bool IsUniqueOrContainsUnique => Unique != null;
public virtual List GetUniqueObjects()
{
if (Unique == null)
return new List();
else
return new List() { this };
}
public bool HasArmorLevel()
{
return ArmorLevel > 0;
}
public virtual bool IsBeingTradedOrContainsItemBeingTraded(HashSet guidList) => guidList.Contains(Guid);
public bool IsSocietyArmor => WieldSkillType >= (int)PropertyInt.SocietyRankCelhan && WieldSkillType <= (int)PropertyInt.SocietyRankRadblo;
public int StructureUnitValue
{
get
{
var weenie = DatabaseManager.World.GetCachedWeenie(WeenieClassId);
var weenieValue = weenie?.GetValue() ?? 0;
var weenieMaxStructure = weenie?.GetMaxStructure() ?? 1;
var structureUnitValue = weenieValue / weenieMaxStructure;
return Math.Max(0, structureUnitValue);
}
}
}
}