using System; using System.Collections.Generic; using log4net; using ACE.Common; using ACE.DatLoader; using ACE.DatLoader.FileTypes; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Entity.Models; using ACE.Server.Entity; using ACE.Server.Managers; using ACE.Server.WorldObjects.Entity; using Position = ACE.Entity.Position; namespace ACE.Server.WorldObjects { public partial class Creature : Container { private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); public bool IsExhausted { get => Stamina.Current == 0; } protected QuestManager _questManager; public QuestManager QuestManager { get { if (_questManager == null) { /*if (!(this is Player)) log.DebugFormat("Initializing non-player QuestManager for {0} (0x{1})", Name, Guid);*/ _questManager = new QuestManager(this); } return _questManager; } } /// /// A table of players who currently have their targeting reticule on this creature /// private Dictionary selectedTargets; /// /// Currently used to handle some edge cases for faction mobs /// DamageHistory.HasDamager() has the following issues: /// - if a player attacks a same-factioned mob but is evaded, the mob would quickly de-aggro /// - if a player attacks a same-factioned mob in a group of same-factioned mobs, the other nearby faction mobs should be alerted, and should maintain aggro, even without a DamageHistory entry /// - if a summoner attacks a same-factioned mob, should the summoned CombatPet possibly defend the player in that situation? /// //public HashSet RetaliateTargets { get; set; } /// /// A new biota be created taking all of its values from weenie. /// public Creature(Weenie weenie, ObjectGuid guid) : base(weenie, guid) { InitializePropertyDictionaries(); SetEphemeralValues(); } /// /// Restore a WorldObject from the database. /// public Creature(Biota biota) : base(biota) { InitializePropertyDictionaries(); SetEphemeralValues(); } private void InitializePropertyDictionaries() { if (Biota.PropertiesAttribute == null) Biota.PropertiesAttribute = new Dictionary(); if (Biota.PropertiesAttribute2nd == null) Biota.PropertiesAttribute2nd = new Dictionary(); if (Biota.PropertiesBodyPart == null) Biota.PropertiesBodyPart = new Dictionary(); if (Biota.PropertiesSkill == null) Biota.PropertiesSkill = new Dictionary(); } private void SetEphemeralValues() { CombatMode = CombatMode.NonCombat; DamageHistory = new DamageHistory(this); if (!(this is Player)) GenerateNewFace(); // If any of the vitals don't exist for this biota, one will be created automatically in the CreatureVital ctor Vitals[PropertyAttribute2nd.MaxHealth] = new CreatureVital(this, PropertyAttribute2nd.MaxHealth); Vitals[PropertyAttribute2nd.MaxStamina] = new CreatureVital(this, PropertyAttribute2nd.MaxStamina); Vitals[PropertyAttribute2nd.MaxMana] = new CreatureVital(this, PropertyAttribute2nd.MaxMana); // If any of the attributes don't exist for this biota, one will be created automatically in the CreatureAttribute ctor Attributes[PropertyAttribute.Strength] = new CreatureAttribute(this, PropertyAttribute.Strength); Attributes[PropertyAttribute.Endurance] = new CreatureAttribute(this, PropertyAttribute.Endurance); Attributes[PropertyAttribute.Coordination] = new CreatureAttribute(this, PropertyAttribute.Coordination); Attributes[PropertyAttribute.Quickness] = new CreatureAttribute(this, PropertyAttribute.Quickness); Attributes[PropertyAttribute.Focus] = new CreatureAttribute(this, PropertyAttribute.Focus); Attributes[PropertyAttribute.Self] = new CreatureAttribute(this, PropertyAttribute.Self); foreach (var kvp in Biota.PropertiesSkill) Skills[kvp.Key] = new CreatureSkill(this, kvp.Key, kvp.Value); if (Health.Current <= 0) Health.Current = Health.MaxValue; if (Stamina.Current <= 0) Stamina.Current = Stamina.MaxValue; if (Mana.Current <= 0) Mana.Current = Mana.MaxValue; if (!(this is Player)) { GenerateWieldList(); EquipInventoryItems(); GenerateWieldedTreasure(); EquipInventoryItems(); GenerateInventoryTreasure(); // TODO: fix tod data Health.Current = Health.MaxValue; Stamina.Current = Stamina.MaxValue; Mana.Current = Mana.MaxValue; } SetMonsterState(); CurrentMotionState = new Motion(MotionStance.NonCombat, MotionCommand.Ready); selectedTargets = new Dictionary(); } // verify logic public bool IsNPC => !(this is Player) && !Attackable && TargetingTactic == TargetingTactic.None; public void GenerateNewFace() { var cg = DatManager.PortalDat.CharGen; if (!Heritage.HasValue) { if (!string.IsNullOrEmpty(HeritageGroupName) && Enum.TryParse(HeritageGroupName.Replace("'", ""), true, out HeritageGroup heritage)) Heritage = (int)heritage; } if (!Gender.HasValue) { if (!string.IsNullOrEmpty(Sex) && Enum.TryParse(Sex, true, out Gender gender)) Gender = (int)gender; } if (!Heritage.HasValue || !Gender.HasValue) { #if DEBUG //if (!(NpcLooksLikeObject ?? false)) //log.DebugFormat("Creature.GenerateNewFace: {0} (0x{1}) - wcid {2} - Heritage: {3} | HeritageGroupName: {4} | Gender: {5} | Sex: {6} - Data missing or unparsable, Cannot randomize face.", Name, Guid, WeenieClassId, Heritage, HeritageGroupName, Gender, Sex); #endif return; } if (!cg.HeritageGroups.TryGetValue((uint)Heritage, out var heritageGroup) || !heritageGroup.Genders.TryGetValue((int)Gender, out var sex)) { #if DEBUG log.DebugFormat("Creature.GenerateNewFace: {0} (0x{1}) - wcid {2} - Heritage: {3} | HeritageGroupName: {4} | Gender: {5} | Sex: {6} - Data invalid, Cannot randomize face.", Name, Guid, WeenieClassId, Heritage, HeritageGroupName, Gender, Sex); #endif return; } PaletteBaseId = sex.BasePalette; var appearance = new Appearance { HairStyle = 1, HairColor = 1, HairHue = 1, EyeColor = 1, Eyes = 1, Mouth = 1, Nose = 1, SkinHue = 1 }; // Get the hair first, because we need to know if you're bald, and that's the name of that tune! if (sex.HairStyleList.Count > 1) { if (PropertyManager.GetBool("npc_hairstyle_fullrange").Item) appearance.HairStyle = (uint)ThreadSafeRandom.Next(0, sex.HairStyleList.Count - 1); else appearance.HairStyle = (uint)ThreadSafeRandom.Next(0, Math.Min(sex.HairStyleList.Count - 1, 8)); // retail range data compiled by OptimShi } else appearance.HairStyle = 0; if (sex.HairStyleList.Count < appearance.HairStyle) { log.Warn($"Creature.GenerateNewFace: {Name} (0x{Guid}) - wcid {WeenieClassId} - HairStyle = {appearance.HairStyle} | HairStyleList.Count = {sex.HairStyleList.Count} - Data invalid, Cannot randomize face."); return; } var hairstyle = sex.HairStyleList[Convert.ToInt32(appearance.HairStyle)]; appearance.HairColor = (uint)ThreadSafeRandom.Next(0, sex.HairColorList.Count - 1); appearance.HairHue = ThreadSafeRandom.Next(0.0f, 1.0f); appearance.EyeColor = (uint)ThreadSafeRandom.Next(0, sex.EyeColorList.Count - 1); appearance.Eyes = (uint)ThreadSafeRandom.Next(0, sex.EyeStripList.Count - 1); appearance.Mouth = (uint)ThreadSafeRandom.Next(0, sex.MouthStripList.Count - 1); appearance.Nose = (uint)ThreadSafeRandom.Next(0, sex.NoseStripList.Count - 1); appearance.SkinHue = ThreadSafeRandom.Next(0.0f, 1.0f); //// Certain races (Undead, Tumeroks, Others?) have multiple body styles available. This is controlled via the "hair style". ////if (hairstyle.AlternateSetup > 0) //// character.SetupTableId = hairstyle.AlternateSetup; if (!EyesTextureDID.HasValue) EyesTextureDID = sex.GetEyeTexture(appearance.Eyes, hairstyle.Bald); if (!DefaultEyesTextureDID.HasValue) DefaultEyesTextureDID = sex.GetDefaultEyeTexture(appearance.Eyes, hairstyle.Bald); if (!NoseTextureDID.HasValue) NoseTextureDID = sex.GetNoseTexture(appearance.Nose); if (!DefaultNoseTextureDID.HasValue) DefaultNoseTextureDID = sex.GetDefaultNoseTexture(appearance.Nose); if (!MouthTextureDID.HasValue) MouthTextureDID = sex.GetMouthTexture(appearance.Mouth); if (!DefaultMouthTextureDID.HasValue) DefaultMouthTextureDID = sex.GetDefaultMouthTexture(appearance.Mouth); if (!HeadObjectDID.HasValue) HeadObjectDID = sex.GetHeadObject(appearance.HairStyle); // Skin is stored as PaletteSet (list of Palettes), so we need to read in the set to get the specific palette var skinPalSet = DatManager.PortalDat.ReadFromDat(sex.SkinPalSet); if (!SkinPaletteDID.HasValue) SkinPaletteDID = skinPalSet.GetPaletteID(appearance.SkinHue); // Hair is stored as PaletteSet (list of Palettes), so we need to read in the set to get the specific palette var hairPalSet = DatManager.PortalDat.ReadFromDat(sex.HairColorList[Convert.ToInt32(appearance.HairColor)]); if (!HairPaletteDID.HasValue) HairPaletteDID = hairPalSet.GetPaletteID(appearance.HairHue); // Eye Color if (!EyesPaletteDID.HasValue) EyesPaletteDID = sex.EyeColorList[Convert.ToInt32(appearance.EyeColor)]; } public virtual float GetBurdenMod() { return 1.0f; // override for players } /// /// This will be false when creature is dead and waits for respawn /// public bool IsAlive { get => Health.Current > 0; } /// /// Sends the network commands to move a player towards an object /// public void MoveToObject(WorldObject target, float? useRadius = null) { var distanceToObject = useRadius ?? target.UseRadius ?? 0.6f; var moveToObject = new Motion(this, target, MovementType.MoveToObject); moveToObject.MoveToParameters.DistanceToObject = distanceToObject; // move directly to portal origin //if (target is Portal) //moveToObject.MoveToParameters.MovementParameters &= ~MovementParams.UseSpheres; SetWalkRunThreshold(moveToObject, target.Location); EnqueueBroadcastMotion(moveToObject); } /// /// Sends the network commands to move a player towards a position /// public void MoveToPosition(Position position) { var moveToPosition = new Motion(this, position); moveToPosition.MoveToParameters.DistanceToObject = 0.0f; SetWalkRunThreshold(moveToPosition, position); EnqueueBroadcastMotion(moveToPosition); } public void SetWalkRunThreshold(Motion motion, Position targetLocation) { // FIXME: WalkRunThreshold (default 15 distance) seems to not be used automatically by client // player will always walk instead of run, and if MovementParams.CanCharge is sent, they will always charge // to remedy this, we manually calculate a threshold based on WalkRunThreshold var dist = Location.DistanceTo(targetLocation); if (dist >= motion.MoveToParameters.WalkRunThreshold / 2.0f) // default 15 distance seems too far, especially with weird in-combat walking animation? { motion.MoveToParameters.MovementParameters |= MovementParams.CanCharge; // TODO: find the correct runrate here // the default runrate / charge seems much too fast... //motion.RunRate = GetRunRate() / 4.0f; motion.RunRate = GetRunRate(); } } /// /// This is raised by Player.HandleActionUseItem. /// The item does not exist in the players possession. /// If the item was outside of range, the player will have been commanded to move using DoMoveTo before ActOnUse is called. /// When this is called, it should be assumed that the player is within range. /// /// This is the OnUse method. This is just an initial implemention. I have put in the turn to action at this point. /// If we are out of use radius, move to the object. Once in range, let's turn the creature toward us and get started. /// Note - we may need to make an NPC class vs monster as using a monster does not make them turn towrad you as I recall. Og II /// Also, once we are reading in the emotes table by weenie - this will automatically customize the behavior for creatures. /// public override void ActOnUse(WorldObject worldObject) { // handled in base.OnActivate -> EmoteManager.OnUse() } public override void OnCollideObject(WorldObject target) { if (target.ReportCollisions == false) return; if (target is Door door) door.OnCollideObject(this); else if (target is Hotspot hotspot) hotspot.OnCollideObject(this); } /// /// Called when a player selects a target /// public bool OnTargetSelected(Player player) { return selectedTargets.TryAdd(player.Guid.Full, new WorldObjectInfo(player)); } /// /// Called when a player deselects a target /// public bool OnTargetDeselected(Player player) { return selectedTargets.Remove(player.Guid.Full); } /// /// Called when a creature's health changes /// public void OnHealthUpdate() { foreach (var kvp in selectedTargets) { var player = kvp.Value.TryGetWorldObject() as Player; if (player?.Session != null) QueryHealth(player.Session); else selectedTargets.Remove(kvp.Key); } } } }