mirror of
https://github.com/ACEmulator/ACE
synced 2026-08-17 12:26:06 -04:00
382 lines
16 KiB
C#
382 lines
16 KiB
C#
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A table of players who currently have their targeting reticule on this creature
|
|
/// </summary>
|
|
private Dictionary<uint, WorldObjectInfo> selectedTargets;
|
|
|
|
/// <summary>
|
|
/// 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?
|
|
/// </summary>
|
|
//public HashSet<uint> RetaliateTargets { get; set; }
|
|
|
|
/// <summary>
|
|
/// A new biota be created taking all of its values from weenie.
|
|
/// </summary>
|
|
public Creature(Weenie weenie, ObjectGuid guid) : base(weenie, guid)
|
|
{
|
|
InitializePropertyDictionaries();
|
|
SetEphemeralValues();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restore a WorldObject from the database.
|
|
/// </summary>
|
|
public Creature(Biota biota) : base(biota)
|
|
{
|
|
InitializePropertyDictionaries();
|
|
SetEphemeralValues();
|
|
}
|
|
|
|
private void InitializePropertyDictionaries()
|
|
{
|
|
if (Biota.PropertiesAttribute == null)
|
|
Biota.PropertiesAttribute = new Dictionary<PropertyAttribute, PropertiesAttribute>();
|
|
if (Biota.PropertiesAttribute2nd == null)
|
|
Biota.PropertiesAttribute2nd = new Dictionary<PropertyAttribute2nd, PropertiesAttribute2nd>();
|
|
if (Biota.PropertiesBodyPart == null)
|
|
Biota.PropertiesBodyPart = new Dictionary<CombatBodyPart, PropertiesBodyPart>();
|
|
if (Biota.PropertiesSkill == null)
|
|
Biota.PropertiesSkill = new Dictionary<Skill, PropertiesSkill>();
|
|
}
|
|
|
|
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<uint, WorldObjectInfo>();
|
|
}
|
|
|
|
// 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<PaletteSet>(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<PaletteSet>(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
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will be false when creature is dead and waits for respawn
|
|
/// </summary>
|
|
public bool IsAlive { get => Health.Current > 0; }
|
|
|
|
/// <summary>
|
|
/// Sends the network commands to move a player towards an object
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends the network commands to move a player towards a position
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is raised by Player.HandleActionUseItem.<para />
|
|
/// The item does not exist in the players possession.<para />
|
|
/// If the item was outside of range, the player will have been commanded to move using DoMoveTo before ActOnUse is called.<para />
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a player selects a target
|
|
/// </summary>
|
|
public bool OnTargetSelected(Player player)
|
|
{
|
|
return selectedTargets.TryAdd(player.Guid.Full, new WorldObjectInfo(player));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a player deselects a target
|
|
/// </summary>
|
|
public bool OnTargetDeselected(Player player)
|
|
{
|
|
return selectedTargets.Remove(player.Guid.Full);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a creature's health changes
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|