diff --git a/OpenDiablo2.Common.UT/OpenDiablo2.Common.UT.csproj b/OpenDiablo2.Common.UT/OpenDiablo2.Common.UT.csproj index 4fa42fd1..21f5eda2 100644 --- a/OpenDiablo2.Common.UT/OpenDiablo2.Common.UT.csproj +++ b/OpenDiablo2.Common.UT/OpenDiablo2.Common.UT.csproj @@ -49,6 +49,7 @@ + diff --git a/OpenDiablo2.Common.UT/UT_EnemyState.cs b/OpenDiablo2.Common.UT/UT_EnemyState.cs new file mode 100644 index 00000000..b4af0bf0 --- /dev/null +++ b/OpenDiablo2.Common.UT/UT_EnemyState.cs @@ -0,0 +1,200 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenDiablo2.Common.Enums; +using OpenDiablo2.Common.Enums.Mobs; +using OpenDiablo2.Common.Models.Mobs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.UT +{ + [TestClass] + public class UT_EnemyState + { + public EnemyState MakeEnemyState(int id, eDifficulty difficulty, Random rand) + { + // does not correspond to any particular enemy in the actual data, just for testing + EnemyTypeConfig config = new EnemyTypeConfig( + InternalName: "TestEnemy", + Name: "TestEnemy1", + Type: "Skeleton", + Descriptor: "", + BaseId: 1, + PopulateId: 1, + Spawned: true, + Beta: false, + Code: "SK", + ClientOnly: false, + NoMap: false, + SizeX: 2, + SizeY: 2, + Height: 3, + NoOverlays: false, + OverlayHeight: 2, + Velocity: 3, + RunVelocity: 6, + CanStealFrom: false, + ColdEffect: 50, + Rarity: true, + MinimumGrouping: 2, + MaximumGrouping: 5, + BaseWeapon: "1hs", + AIParams: new int[] {75, 85, 9, 50, 0 }, + Allied: false, + IsNPC: false, + IsCritter: false, + CanEnterTown: false, + HealthRegen: 80, + IsDemon: false, + IsLarge: false, + IsSmall: false, + IsFlying: false, + CanOpenDoors: false, + SpawningColumn: 0, + IsBoss: false, + IsInteractable: false, + IsKillable: true, + CanBeConverted: true, + HitClass: 3, + HasSpecialEndDeath: false, + DeadCollision: false, + CanBeRevivedByOtherMonsters: true, + AppearanceConfig: new EnemyTypeAppearanceConfig( + HasDeathAnimation: true, + HasNeutralAnimation: true, + HasWalkAnimation: true, + HasGetHitAnimation: true, + HasAttack1Animation: true, + HasAttack2Animation: false, + HasBlockAnimation: true, + HasCastAnimation: false, + HasSkillAnimation: new bool[] { false, false, false, false }, + HasCorpseAnimation: false, + HasKnockbackAnimation: true, + HasRunAnimation: true, + HasLifeBar: true, + HasNameBar: false, + CannotBeSelected: false, + CanCorpseBeSelected: false, + BleedType: 0, + HasShadow: true, + LightRadius: 0, + HasUniqueBossColors: false, + CompositeDeath: true, + LightRGB: new byte[] { 255, 255, 255 } + ), + CombatConfig: new EnemyTypeCombatConfig( + ElementalAttackMode: 4, + ElementalAttackType: eDamageTypes.PHYSICAL, + ElementalOverlayId: 0, + ElementalChance: 0, + ElementalMinDamage: 0, + ElementalMaxDamage: 0, + ElementalDuration: 0, + MissileForAttack: new int[] { 0, 0 }, + MissileForSkill: new int[] { 0, 0, 0, 0, }, + MissileForCase: 0, + MissileForSequence: 0, + CanMoveAttack: new bool[] { false, false }, + CanMoveSkill: new bool[] { false, false, false, false }, + CanMoveCast: false, + IsMelee: true, + IsAttackable: true, + MeleeRange: 0, + SkillType: new int[] { 0, 0, 0, 0 }, + SkillSequence: new int[] { 0, 0, 0, 0 }, + SkillLevel: new int[] { 0, 0, 0, 0 }, + IsUndeadWithPhysicalAttacks: true, + IsUndeadWithMagicAttacks: false, + UsesMagicAttacks: false, + ChanceToBlock: 20, + DoesDeathDamage: false, + IgnoredBySummons: false + ), + NormalDifficultyConfig: new EnemyTypeDifficultyConfig( + Level: 5, + DamageResist: 0, + MagicResist: 0, + FireResist: 0.5, + LightResist: 0, + ColdResist: 0, + PoisonResist: 0, + MinHP: 10, + MaxHP: 15, + AC: 3, + Exp: 100, + AttackMinDamage: new int[] { 5, 0 }, + AttackMaxDamage: new int[] { 10, 0 }, + AttackChanceToHit: new int[] { 35, 0 }, + Skill1MinDamage: 0, + Skill1MaxDamage: 0, + Skill1ChanceToHit: 0, + TreasureClass: new string[] { "Swarm 1", "Act 2 Champ A", "Act 2 Unique A", "" } + ), + NightmareDifficultyConfig: new EnemyTypeDifficultyConfig( + Level: 10, + DamageResist: 0, + MagicResist: 0, + FireResist: 0.5, + LightResist: 0.5, + ColdResist: 0, + PoisonResist: 0, + MinHP: 20, + MaxHP: 25, + AC: 3, + Exp: 1000, + AttackMinDamage: new int[] { 5, 0 }, + AttackMaxDamage: new int[] { 10, 0 }, + AttackChanceToHit: new int[] { 35, 0 }, + Skill1MinDamage: 0, + Skill1MaxDamage: 0, + Skill1ChanceToHit: 0, + TreasureClass: new string[] { "Swarm 1", "Act 3 Champ A", "Act 3 Unique A", "" } + ), + HellDifficultyConfig: new EnemyTypeDifficultyConfig( + Level: 15, + DamageResist: 0, + MagicResist: 0, + FireResist: 0.5, + LightResist: 0.5, + ColdResist: 0.5, + PoisonResist: 0, + MinHP: 30, + MaxHP: 45, + AC: 3, + Exp: 10000, + AttackMinDamage: new int[] { 5, 0 }, + AttackMaxDamage: new int[] { 10, 0 }, + AttackChanceToHit: new int[] { 35, 0 }, + Skill1MinDamage: 0, + Skill1MaxDamage: 0, + Skill1ChanceToHit: 0, + TreasureClass: new string[] { "Swarm 2", "Act 4 Champ A", "Act 4 Unique A", "" } + ) + ); + + EnemyState en = new EnemyState("Fallen", id, 0, 0, config, difficulty, rand); + + return en; + } + + [TestMethod] + public void EnemyDifficultyTest() + { + Random rand = new Random(); + EnemyState en = MakeEnemyState(1, eDifficulty.NORMAL, rand); + Assert.AreEqual(5, en.Level); + Assert.AreEqual(100, en.ExperienceGiven); + + EnemyState en2 = MakeEnemyState(2, eDifficulty.NIGHTMARE, rand); + Assert.AreEqual(10, en2.Level); + Assert.AreEqual(1000, en2.ExperienceGiven); + + EnemyState en3 = MakeEnemyState(3, eDifficulty.HELL, rand); + Assert.AreEqual(15, en3.Level); + Assert.AreEqual(10000, en3.ExperienceGiven); + } + } +} diff --git a/OpenDiablo2.Common/Enums/Mobs/eDamageTypes.cs b/OpenDiablo2.Common/Enums/Mobs/eDamageTypes.cs index e59e35c8..92f43999 100644 --- a/OpenDiablo2.Common/Enums/Mobs/eDamageTypes.cs +++ b/OpenDiablo2.Common/Enums/Mobs/eDamageTypes.cs @@ -8,12 +8,19 @@ namespace OpenDiablo2.Common.Enums.Mobs { public enum eDamageTypes { - NONE, // no resistances apply - PHYSICAL, - MAGIC, - FIRE, - COLD, - LIGHTNING, - POISON, + NONE = -1, // no resistances apply + PHYSICAL = 0, + MAGIC = 3, //1=fire, 2=lightning, 4=cold, 5=poison + FIRE = 1, + COLD = 4, + LIGHTNING = 2, + POISON = 5, + LIFE_STEAL = 6, + MANA_STEAL = 7, + STAMINA_STEAL = 8, + STUN = 9, + RANDOM = 10, // random between fire/cold/lightning/poison + BURN = 11, + FREEZE = 12, } } diff --git a/OpenDiablo2.Common/Enums/Mobs/eMobFlags.cs b/OpenDiablo2.Common/Enums/Mobs/eMobFlags.cs index 8d58e354..06fdde64 100644 --- a/OpenDiablo2.Common/Enums/Mobs/eMobFlags.cs +++ b/OpenDiablo2.Common/Enums/Mobs/eMobFlags.cs @@ -20,6 +20,14 @@ namespace OpenDiablo2.Common.Enums.Mobs { PLAYER, ENEMY, - INVULNERABLE + INVULNERABLE, + BOSS, + NPC, + CRITTER, + LARGE, + SMALL, + INTERACTABLE, + DEMON, + IGNORED_BY_SUMMONS, } } diff --git a/OpenDiablo2.Common/Enums/eDifficulty.cs b/OpenDiablo2.Common/Enums/eDifficulty.cs new file mode 100644 index 00000000..a6f52c50 --- /dev/null +++ b/OpenDiablo2.Common/Enums/eDifficulty.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Enums +{ + public enum eDifficulty + { + NORMAL, + NIGHTMARE, + HELL, + } +} diff --git a/OpenDiablo2.Common/Interfaces/Drawing/ICharacterRenderer.cs b/OpenDiablo2.Common/Interfaces/Drawing/ICharacterRenderer.cs index c34b2846..f431f8ba 100644 --- a/OpenDiablo2.Common/Interfaces/Drawing/ICharacterRenderer.cs +++ b/OpenDiablo2.Common/Interfaces/Drawing/ICharacterRenderer.cs @@ -12,6 +12,7 @@ namespace OpenDiablo2.Common.Interfaces.Drawing eHero Hero { get; set; } PlayerEquipment Equipment { get; set; } eMobMode MobMode { get; set; } + void Update(long ms); void Render(int pixelOffsetX, int pixelOffsetY); void ResetAnimationData(); diff --git a/OpenDiablo2.Common/Interfaces/IEngineDataManager.cs b/OpenDiablo2.Common/Interfaces/IEngineDataManager.cs index 20cb52bd..a7c8a357 100644 --- a/OpenDiablo2.Common/Interfaces/IEngineDataManager.cs +++ b/OpenDiablo2.Common/Interfaces/IEngineDataManager.cs @@ -14,5 +14,6 @@ namespace OpenDiablo2.Common.Interfaces ImmutableList Items { get; } ImmutableDictionary ExperienceConfigs { get; } ImmutableDictionary HeroTypeConfigs { get; } + ImmutableList EnemyTypeConfigs { get; } } } diff --git a/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeAppearanceConfig.cs b/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeAppearanceConfig.cs new file mode 100644 index 00000000..ead61b4e --- /dev/null +++ b/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeAppearanceConfig.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Interfaces.Mobs +{ + public interface IEnemyTypeAppearanceConfig + { + bool HasDeathAnimation { get; } + bool HasNeutralAnimation { get; } + bool HasWalkAnimation { get; } + bool HasGetHitAnimation { get; } + bool[] HasAttackAnimation { get; } // 1-2 + bool HasBlockAnimation { get; } + bool HasCastAnimation { get; } + bool[] HasSkillAnimation { get; } // 1-4 + bool HasCorpseAnimation { get; } + bool HasKnockbackAnimation { get; } + // HasSkillSequenceAnimation skipped due to being unused + bool HasRunAnimation { get; } + + bool HasLifeBar { get; } // does this monster show a lifebar when you scroll over it + bool HasNameBar { get; } // does this monster show a name when you scroll over it + bool CannotBeSelected { get; } // if true, monster can never be highlighted + bool CanCorpseBeSelected { get; } // if true, the corpse can be highlighted + + int BleedType { get; } // how does this monster bleed when hit? + // 0 = it doesn't, 1 = small blood, 2 = large blood, 3+ = random missiles from missiles.txt, the larger + // the number, the more missiles (??? this needs to be tested...) + bool HasShadow { get; } // does this have a shadow? + int LightRadius { get; } // how large of a light does this emit? + bool HasUniqueBossColors { get; } // if true, has unique colors when spawned + // as a boss. + bool CompositeDeath { get; } // if true, death animation uses multiple components + // (not clear if this is just for reference or if it actually has an effect...) + + byte[] LightRGB { get; } // 0: R, 1: G, 2: B + } +} diff --git a/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeCombatConfig.cs b/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeCombatConfig.cs new file mode 100644 index 00000000..002e6f5c --- /dev/null +++ b/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeCombatConfig.cs @@ -0,0 +1,54 @@ +using OpenDiablo2.Common.Enums.Mobs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Interfaces.Mobs +{ + public interface IEnemyTypeCombatConfig + { + int ElementalAttackMode { get;} // 4 = on hit, rest are unknown? + eDamageTypes ElementalAttackType { get;} // 1=fire, 2=lightning, 4=cold, 5=poison + int ElementalOverlayId { get;} // see overlays.txt, corresponds to a row number -2 + int ElementalChance { get;} // chance of use on a normal attack, 0 = never 100 = always + int ElementalMinDamage { get;} + int ElementalMaxDamage { get;} + int ElementalDuration { get;} // duration of effects like cold and poison + + // TODO: these could be switched to SHORTS, is there any benefit to that? + int[] MissileForAttack { get;} // 1-2, which missile is used per attack + // important note: 65535 = NONE. See missiles.txt, corresponds to a row number -2 + int[] MissileForSkill { get;} // 1-4, which missile is used for a skill + // important note: 65535 = NONE. See missiles.txt, corresponds to a row number -2 + int MissileForCase { get;} // see above, specifies a missile for Case + int MissileForSequence { get;} // see above, specifies a missile for Sequence + + bool[] CanMoveAttack { get;} // 1-2, can move while using attack 1 / 2 + bool[] CanMoveSkill { get;} // 1-4, can move while using skill 1/2/3/4 + bool CanMoveCast { get;} // can move while casting? + + bool IsMelee { get;} // is this a melee attacker? + bool IsAttackable { get;} // is this monster attackable? + + int MeleeRange { get; } + + int[] SkillType { get;} // 1-5, what type of skill is it? 0 = none + int[] SkillSequence { get;} // 1-5 animation sequence number + // should not be 0 if skill is not 0; see animation column in skills.txt + int[] SkillLevel { get;} // 1-5, level of the skill + + bool IsUndeadWithPhysicalAttacks { get;} // if true, is an undead attacking with physical attacks + bool IsUndeadWithMagicAttacks { get;} // if true '' magic atttacks + bool UsesMagicAttacks { get;} + + int ChanceToBlock { get;} + + bool DoesDeathDamage { get;} // if true, does damage when it dies + // like undead flayers (TODO: understand exactly how this works) + + bool IgnoredBySummons { get;} // if true, is ignored by summons in combat + + } +} diff --git a/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeConfig.cs b/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeConfig.cs new file mode 100644 index 00000000..1c1a40ab --- /dev/null +++ b/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeConfig.cs @@ -0,0 +1,88 @@ +using OpenDiablo2.Common.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Interfaces.Mobs +{ + public interface IEnemyTypeConfig + { + string InternalName { get; } + string DisplayName { get; } // Note the distinction here; the second column + // in the monstats is a unique identifier string, whereas the first is non-unique + string Type { get; } + string Descriptor { get; } + + int BaseId { get; } + int PopulateId { get; } + bool Spawned { get; } + bool Beta { get; } + string Code { get; } + bool ClientOnly { get; } + bool NoMap { get; } + + int SizeX { get; } + int SizeY { get; } + int Height { get; } + bool NoOverlays { get; } + int OverlayHeight { get; } + + int Velocity { get; } + int RunVelocity { get; } + + bool CanStealFrom { get; } + int ColdEffect { get; } + + bool Rarity { get; } + + int MinimumGrouping { get; } + int MaximumGrouping { get; } + + string BaseWeapon { get; } + + int[] AIParams { get; } // up to 5 + + bool Allied { get; } // Is this enemy on the player's side? + bool IsNPC { get; } // is this an NPC? + bool IsCritter { get; } // is this a critter? (e.g. the chickens) + bool CanEnterTown { get; } // can this enter town or not? + + int HealthRegen { get; } // hp regen per minute + + bool IsDemon { get; } // demon? + bool IsLarge { get; } // size large (e.g. bosses) + bool IsSmall { get; } // size small (e.g. fallen) + bool IsFlying { get; } // can move over water for instance + bool CanOpenDoors { get; } + int SpawningColumn { get; } // is the monster area restricted + // 0 = no, spawns through levels.txt, 1-3 unknown? + // TODO: understand spawningcolumn + bool IsBoss { get; } + bool IsInteractable { get; } // can this be interacted with? like an NPC + + bool IsKillable { get; } + bool CanBeConverted { get; } // if true, can be switched to Allied by spells like Conversion + + int HitClass { get; } // TODO: find out what this is + + bool HasSpecialEndDeath { get; } // marks if a monster dies a special death + bool DeadCollision { get; } // if true, corpse has collision + + bool CanBeRevivedByOtherMonsters { get; } + + // appearance config + IEnemyTypeAppearanceConfig AppearanceConfig { get; } + + // combat config + IEnemyTypeCombatConfig CombatConfig { get; } + + // difficulty configs + IEnemyTypeDifficultyConfig NormalDifficultyConfig { get; } + IEnemyTypeDifficultyConfig NightmareDifficultyConfig { get; } + IEnemyTypeDifficultyConfig HellDifficultyConfig { get; } + + IEnemyTypeDifficultyConfig GetDifficultyConfig(eDifficulty Difficulty); + } +} diff --git a/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeDifficultyConfig.cs b/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeDifficultyConfig.cs new file mode 100644 index 00000000..65f8c23b --- /dev/null +++ b/OpenDiablo2.Common/Interfaces/Mobs/IEnemyTypeDifficultyConfig.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Interfaces.Mobs +{ + public interface IEnemyTypeDifficultyConfig + { + int Level { get; } + + double DamageResist { get; } + double MagicResist { get; } + double FireResist { get; } + double LightningResist { get; } + double ColdResist { get; } + double PoisonResist { get; } + + int MinHP { get; } + int MaxHP { get; } + int AC { get; } // armor class + int Exp { get; } + + int[] AttackMinDamage { get; } // 1-2, min damage attack can roll + int[] AttackMaxDamage { get; } // 1-2 max damage attack can roll + int[] AttackChanceToHit { get; } // 1-2 chance attack has to hit (out of 100??) + int Skill1MinDamage { get; } // min damage skill 1 can do (why only skill 1?) + int Skill1MaxDamage { get; } // max damage for skill 1 + int Skill1ChanceToHit { get; } // chance skill has to hit + + string[] TreasureClass { get; } // 1-4 + } +} diff --git a/OpenDiablo2.Common/Models/Mobs/EnemyState.cs b/OpenDiablo2.Common/Models/Mobs/EnemyState.cs index 96445f88..2cdeb201 100644 --- a/OpenDiablo2.Common/Models/Mobs/EnemyState.cs +++ b/OpenDiablo2.Common/Models/Mobs/EnemyState.cs @@ -1,17 +1,95 @@ -using OpenDiablo2.Common.Enums.Mobs; +using OpenDiablo2.Common.Enums; +using OpenDiablo2.Common.Enums.Mobs; +using OpenDiablo2.Common.Interfaces.Mobs; +using System; namespace OpenDiablo2.Common.Models.Mobs { public class EnemyState : MobState { + public IEnemyTypeConfig EnemyConfig { get; protected set; } + public int ExperienceGiven { get; protected set; } + + public Stat[] AttackRating { get; protected set; } + public Stat DefenseRating { get; protected set; } + public Stat HealthRegen { get; protected set; } + + public eDifficulty CreatedDifficulty { get; protected set; } public EnemyState() : base() { } - public EnemyState(string name, int id, int level, int maxhealth, float x, float y, int experiencegiven) - : base(name, id, level, maxhealth, x, y) + public EnemyState(string name, int id, float x, float y, + IEnemyTypeConfig EnemyConfig, eDifficulty Difficulty, Random Rand) + : base(name, id, 0, 0, x, y) { - ExperienceGiven = experiencegiven; + this.EnemyConfig = EnemyConfig; + IEnemyTypeDifficultyConfig difficultyConfig = EnemyConfig.GetDifficultyConfig(Difficulty); + CreatedDifficulty = Difficulty; + + int maxhp = Rand.Next(difficultyConfig.MinHP, difficultyConfig.MaxHP + 1); + Health = new Stat(0, maxhp, maxhp, true); + ExperienceGiven = difficultyConfig.Exp; + Level = difficultyConfig.Level; + + // stats + AttackRating = new Stat[2]; + AttackRating[0] = new Stat(0, difficultyConfig.AttackChanceToHit[0], difficultyConfig.AttackChanceToHit[0], false); + AttackRating[1] = new Stat(0, difficultyConfig.AttackChanceToHit[1], difficultyConfig.AttackChanceToHit[1], false); + + DefenseRating = new Stat(0, EnemyConfig.CombatConfig.ChanceToBlock, EnemyConfig.CombatConfig.ChanceToBlock, false); + + HealthRegen = new Stat(0, EnemyConfig.HealthRegen, EnemyConfig.HealthRegen, true); + + // handle immunities / resistances + SetResistance(eDamageTypes.COLD, difficultyConfig.ColdResist); + SetResistance(eDamageTypes.FIRE, difficultyConfig.FireResist); + SetResistance(eDamageTypes.MAGIC, difficultyConfig.MagicResist); + SetResistance(eDamageTypes.PHYSICAL, difficultyConfig.DamageResist); + SetResistance(eDamageTypes.POISON, difficultyConfig.PoisonResist); + SetResistance(eDamageTypes.LIGHTNING, difficultyConfig.LightningResist); + // interestingly, monsters don't actually have immunity flags + // TODO: should we treat a resistance of 1 differently? + // should a resistance of 1 be an immunity (e.g. the main difference is that + // effects / debuffs couldn't reduce the resistance below 100%) + + // handle flags + if (EnemyConfig.IsBoss) + { + AddFlag(eMobFlags.BOSS); + } + if (EnemyConfig.IsCritter) + { + AddFlag(eMobFlags.CRITTER); + } + if (EnemyConfig.IsNPC) + { + AddFlag(eMobFlags.NPC); + } + if (EnemyConfig.IsLarge) + { + AddFlag(eMobFlags.LARGE); + } + if (EnemyConfig.IsSmall) + { + AddFlag(eMobFlags.SMALL); + } + if (EnemyConfig.IsInteractable) + { + AddFlag(eMobFlags.INTERACTABLE); + } + if (!EnemyConfig.IsKillable) + { + AddFlag(eMobFlags.INVULNERABLE); + } + if (EnemyConfig.IsDemon) + { + AddFlag(eMobFlags.DEMON); + } + if (EnemyConfig.CombatConfig.IgnoredBySummons) + { + AddFlag(eMobFlags.IGNORED_BY_SUMMONS); + } AddFlag(eMobFlags.ENEMY); } } diff --git a/OpenDiablo2.Common/Models/Mobs/EnemyTypeAppearanceConfig.cs b/OpenDiablo2.Common/Models/Mobs/EnemyTypeAppearanceConfig.cs new file mode 100644 index 00000000..976fa10e --- /dev/null +++ b/OpenDiablo2.Common/Models/Mobs/EnemyTypeAppearanceConfig.cs @@ -0,0 +1,77 @@ +using OpenDiablo2.Common.Interfaces.Mobs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Models.Mobs +{ + public class EnemyTypeAppearanceConfig : IEnemyTypeAppearanceConfig + { + public bool HasDeathAnimation { get; private set; } + public bool HasNeutralAnimation { get; private set; } + public bool HasWalkAnimation { get; private set; } + public bool HasGetHitAnimation { get; private set; } + public bool[] HasAttackAnimation { get; private set; } // 1-2 + public bool HasBlockAnimation { get; private set; } + public bool HasCastAnimation { get; private set; } + public bool[] HasSkillAnimation { get; private set; } // 1-4 + public bool HasCorpseAnimation { get; private set; } + public bool HasKnockbackAnimation { get; private set; } + // HasSkillSequenceAnimation skipped due to being unused + public bool HasRunAnimation { get; private set; } + + public bool HasLifeBar { get; private set; } // does this monster show a lifebar when you scroll over it + public bool HasNameBar { get; private set; } // does this monster show a name when you scroll over it + public bool CannotBeSelected { get; private set; } // if true, monster can never be highlighted + public bool CanCorpseBeSelected { get; private set; } // if true, the corpse can be highlighted + + public int BleedType { get; private set; } // how does this monster bleed when hit? + // 0 = it doesn't, 1 = small blood, 2 = large blood, 3+ = random missiles from missiles.txt, the larger + // the number, the more missiles (??? this needs to be tested...) + public bool HasShadow { get; private set; } // does this have a shadow? + public int LightRadius { get; private set; } // how large of a light does this emit? + public bool HasUniqueBossColors { get; private set; } // if true, has unique colors when spawned + // as a boss. + public bool CompositeDeath { get; private set; } // if true, death animation uses multiple components + // (not clear if this is just for reference or if it actually has an effect...) + + public byte[] LightRGB { get; private set; } // 0: R, 1: G, 2: B + + public EnemyTypeAppearanceConfig(bool HasDeathAnimation, bool HasNeutralAnimation, + bool HasWalkAnimation, bool HasGetHitAnimation, bool HasAttack1Animation, + bool HasAttack2Animation, bool HasBlockAnimation, bool HasCastAnimation, + bool[] HasSkillAnimation, bool HasCorpseAnimation, bool HasKnockbackAnimation, + bool HasRunAnimation, + bool HasLifeBar, bool HasNameBar, bool CannotBeSelected, bool CanCorpseBeSelected, + int BleedType, bool HasShadow, int LightRadius, bool HasUniqueBossColors, bool CompositeDeath, + byte[] LightRGB) + { + this.HasDeathAnimation = HasDeathAnimation; + this.HasNeutralAnimation = HasNeutralAnimation; + this.HasWalkAnimation = HasWalkAnimation; + this.HasGetHitAnimation = HasGetHitAnimation; + this.HasAttackAnimation = new bool[] { HasAttack1Animation, HasAttack2Animation }; + this.HasBlockAnimation = HasBlockAnimation; + this.HasCastAnimation = HasCastAnimation; + this.HasSkillAnimation = HasSkillAnimation; + this.HasCorpseAnimation = HasCorpseAnimation; + this.HasKnockbackAnimation = HasKnockbackAnimation; + this.HasRunAnimation = HasRunAnimation; + + this.HasLifeBar = HasLifeBar; + this.HasNameBar = HasNameBar; + this.CannotBeSelected = CannotBeSelected; + this.CanCorpseBeSelected = CanCorpseBeSelected; + + this.BleedType = BleedType; + this.HasShadow = HasShadow; + this.LightRadius = LightRadius; + this.HasUniqueBossColors = HasUniqueBossColors; + this.CompositeDeath = CompositeDeath; + + this.LightRGB = LightRGB; + } + } +} diff --git a/OpenDiablo2.Common/Models/Mobs/EnemyTypeCombatConfig.cs b/OpenDiablo2.Common/Models/Mobs/EnemyTypeCombatConfig.cs new file mode 100644 index 00000000..1a1125ea --- /dev/null +++ b/OpenDiablo2.Common/Models/Mobs/EnemyTypeCombatConfig.cs @@ -0,0 +1,109 @@ +using OpenDiablo2.Common.Enums.Mobs; +using OpenDiablo2.Common.Interfaces.Mobs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Models.Mobs +{ + public class EnemyTypeCombatConfig : IEnemyTypeCombatConfig + { + public int ElementalAttackMode { get; private set; } // 4 = on hit, rest are unknown? + public eDamageTypes ElementalAttackType { get; private set; } // 1=fire, 2=lightning, 4=cold, 5=poison + public int ElementalOverlayId { get; private set; } // see overlays.txt, corresponds to a row number -2 + public int ElementalChance { get; private set; } // chance of use on a normal attack, 0 = never 100 = always + public int ElementalMinDamage { get; private set; } + public int ElementalMaxDamage { get; private set; } + public int ElementalDuration { get; private set; } // duration of effects like cold and poison + + // TODO: these could be switched to SHORTS, is there any benefit to that? + public int[] MissileForAttack { get; private set; } // 1-2, which missile is used per attack + // important note: 65535 = NONE. See missiles.txt, corresponds to a row number -2 + public int[] MissileForSkill { get; private set; } // 1-4, which missile is used for a skill + // important note: 65535 = NONE. See missiles.txt, corresponds to a row number -2 + public int MissileForCase { get; private set; } // see above, specifies a missile for Case + public int MissileForSequence { get; private set; } // see above, specifies a missile for Sequence + + public bool[] CanMoveAttack { get; private set; } // 1-2, can move while using attack 1 / 2 + public bool[] CanMoveSkill { get; private set; } // 1-4, can move while using skill 1/2/3/4 + public bool CanMoveCast { get; private set; } // can move while casting? + + public bool IsMelee { get; private set; } // is this a melee attacker? + public bool IsAttackable { get; private set; } // is this monster attackable? + + public int MeleeRange { get; private set; } + + public int[] SkillType { get; private set; } // 1-5, what type of skill is it? 0 = none + public int[] SkillSequence { get; private set; } // 1-5 animation sequence number + // should not be 0 if skill is not 0; see animation column in skills.txt + public int[] SkillLevel { get; private set; } // 1-5, level of the skill + + public bool IsUndeadWithPhysicalAttacks { get; private set; } // if true, is an undead attacking with physical attacks + public bool IsUndeadWithMagicAttacks { get; private set; } // if true '' magic atttacks + public bool UsesMagicAttacks { get; private set; } + + public int ChanceToBlock { get; private set; } + + public bool DoesDeathDamage { get; private set; } // if true, does damage when it dies + // like undead flayers (TODO: understand exactly how this works) + + public bool IgnoredBySummons { get; private set; } // if true, is ignored by summons in combat + + // (87) A1Move A2Move S1Move S2Move S3Move S4Move Cmove + // (109) Skill1 Skill1Seq Skill1Lvl Skill2 Skill2Seq Skill2Lvl Skill3 Skill3Seq Skill3Lvl Skill4 Skill4Seq Skill4Lvl Skill5 Skill5Seq Skill5Lvl + // (146) eLUndead eHUndead eDemon eMagicUsing eLarge eSmall eFlying eOpenDoors eSpawnCol eBoss + + + public EnemyTypeCombatConfig(int ElementalAttackMode, eDamageTypes ElementalAttackType, int ElementalOverlayId, + int ElementalChance, int ElementalMinDamage, int ElementalMaxDamage, int ElementalDuration, + int[] MissileForAttack, int[] MissileForSkill, int MissileForCase, int MissileForSequence, + bool[] CanMoveAttack, bool[] CanMoveSkill, bool CanMoveCast, + bool IsMelee, bool IsAttackable, + int MeleeRange, + int[] SkillType, int[] SkillSequence, int[] SkillLevel, + bool IsUndeadWithPhysicalAttacks, bool IsUndeadWithMagicAttacks, bool UsesMagicAttacks, + int ChanceToBlock, + bool DoesDeathDamage, + bool IgnoredBySummons + ) + { + this.ElementalAttackMode = ElementalAttackMode; + this.ElementalAttackType = ElementalAttackType; + this.ElementalOverlayId = ElementalOverlayId; + this.ElementalChance = ElementalChance; + this.ElementalMinDamage = ElementalMinDamage; + this.ElementalMaxDamage = ElementalMaxDamage; + this.ElementalDuration = ElementalDuration; + + this.MissileForAttack = MissileForAttack; + this.MissileForSkill = MissileForSkill; + this.MissileForCase = MissileForCase; + this.MissileForSequence = MissileForSequence; + + this.CanMoveAttack = CanMoveAttack; + this.CanMoveSkill = CanMoveSkill; + this.CanMoveCast = CanMoveCast; + + this.IsMelee = IsMelee; + this.IsAttackable = IsAttackable; + + this.MeleeRange = MeleeRange; + + this.SkillType = SkillType; + this.SkillSequence = SkillSequence; + this.SkillLevel = SkillLevel; + + this.IsUndeadWithPhysicalAttacks = IsUndeadWithPhysicalAttacks; + this.IsUndeadWithMagicAttacks = IsUndeadWithMagicAttacks; + this.UsesMagicAttacks = UsesMagicAttacks; + + this.ChanceToBlock = ChanceToBlock; + + this.DoesDeathDamage = DoesDeathDamage; + + this.IgnoredBySummons = IgnoredBySummons; + } + } +} diff --git a/OpenDiablo2.Common/Models/Mobs/EnemyTypeConfig.cs b/OpenDiablo2.Common/Models/Mobs/EnemyTypeConfig.cs new file mode 100644 index 00000000..f07c073e --- /dev/null +++ b/OpenDiablo2.Common/Models/Mobs/EnemyTypeConfig.cs @@ -0,0 +1,586 @@ +using OpenDiablo2.Common.Enums; +using OpenDiablo2.Common.Enums.Mobs; +using OpenDiablo2.Common.Interfaces.Mobs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Models.Mobs +{ + public class EnemyTypeConfig : IEnemyTypeConfig + { + public string InternalName { get; private set; } + public string DisplayName { get; private set; } + public string Type { get; private set; } + public string Descriptor { get; private set; } + + public int BaseId { get; private set; } + public int PopulateId { get; private set; } + public bool Spawned { get; private set; } + public bool Beta { get; private set; } + public string Code { get; private set; } + public bool ClientOnly { get; private set; } + public bool NoMap { get; private set; } + + public int SizeX { get; private set; } + public int SizeY { get; private set; } + public int Height { get; private set; } + public bool NoOverlays { get; private set; } + public int OverlayHeight { get; private set; } + + public int Velocity { get; private set; } + public int RunVelocity { get; private set; } + + public bool CanStealFrom { get; private set; } + public int ColdEffect { get; private set; } + + public bool Rarity { get; private set; } + + public int MinimumGrouping { get; private set; } + public int MaximumGrouping { get; private set; } + + public string BaseWeapon { get; private set; } + + public int[] AIParams { get; private set; } // up to 5 + + public bool Allied { get; private set; } // Is this enemy on the player's side? + public bool IsNPC { get; private set; } // is this an NPC? + public bool IsCritter { get; private set; } // is this a critter? (e.g. the chickens) + public bool CanEnterTown { get; private set; } // can this enter town or not? + + public int HealthRegen { get; private set; } // hp regen per minute + + public bool IsDemon { get; private set; } // demon? + public bool IsLarge { get; private set; } // size large (e.g. bosses) + public bool IsSmall { get; private set; } // size small (e.g. fallen) + public bool IsFlying { get; private set; } // can move over water for instance + public bool CanOpenDoors { get; private set; } + public int SpawningColumn { get; private set; } // is the monster area restricted + // 0 = no, spawns through levels.txt, 1-3 unknown? + // TODO: understand spawningcolumn + public bool IsBoss { get; private set; } + public bool IsInteractable { get; private set; } // can this be interacted with? like an NPC + + public bool IsKillable { get; private set; } + public bool CanBeConverted { get; private set; } // if true, can be switched to Allied by spells like Conversion + + public int HitClass { get; private set; } // TODO: find out what this is + + public bool HasSpecialEndDeath { get; private set; } // marks if a monster dies a special death + public bool DeadCollision { get; private set; } // if true, corpse has collision + + public bool CanBeRevivedByOtherMonsters { get; private set; } + + // appearance config + public IEnemyTypeAppearanceConfig AppearanceConfig { get; private set; } + + // combat config + public IEnemyTypeCombatConfig CombatConfig { get; private set; } + + // difficulty configs + public IEnemyTypeDifficultyConfig NormalDifficultyConfig { get; private set; } + public IEnemyTypeDifficultyConfig NightmareDifficultyConfig { get; private set; } + public IEnemyTypeDifficultyConfig HellDifficultyConfig { get; private set; } + + public EnemyTypeConfig(string InternalName, string Name, string Type, string Descriptor, + int BaseId, int PopulateId, bool Spawned, bool Beta, string Code, bool ClientOnly, bool NoMap, + int SizeX, int SizeY, int Height, bool NoOverlays, int OverlayHeight, + int Velocity, int RunVelocity, + bool CanStealFrom, int ColdEffect, + bool Rarity, + int MinimumGrouping, int MaximumGrouping, + string BaseWeapon, + int[] AIParams, + bool Allied, bool IsNPC, bool IsCritter, bool CanEnterTown, + int HealthRegen, + bool IsDemon, bool IsLarge, bool IsSmall, bool IsFlying, bool CanOpenDoors, int SpawningColumn, + bool IsBoss, bool IsInteractable, + bool IsKillable, bool CanBeConverted, + int HitClass, + bool HasSpecialEndDeath, bool DeadCollision, + bool CanBeRevivedByOtherMonsters, + IEnemyTypeAppearanceConfig AppearanceConfig, + IEnemyTypeCombatConfig CombatConfig, + IEnemyTypeDifficultyConfig NormalDifficultyConfig, + IEnemyTypeDifficultyConfig NightmareDifficultyConfig, + IEnemyTypeDifficultyConfig HellDifficultyConfig) + { + this.InternalName = InternalName; + this.DisplayName = Name; + this.Type = Type; + this.Descriptor = Descriptor; + + this.BaseId = BaseId; + this.PopulateId = PopulateId; + this.Spawned = Spawned; + this.Beta = Beta; + this.Code = Code; + this.ClientOnly = ClientOnly; + this.NoMap = NoMap; + + this.SizeX = SizeX; + this.SizeY = SizeY; + this.Height = Height; + this.NoOverlays = NoOverlays; + this.OverlayHeight = OverlayHeight; + + this.Velocity = Velocity; + this.RunVelocity = RunVelocity; + + this.CanStealFrom = CanStealFrom; + this.ColdEffect = ColdEffect; + + this.Rarity = Rarity; + + this.MinimumGrouping = MinimumGrouping; + this.MaximumGrouping = MaximumGrouping; + + this.BaseWeapon = BaseWeapon; + + this.AIParams = AIParams; + + this.Allied = Allied; + this.IsNPC = IsNPC; + this.IsCritter = IsCritter; + this.CanEnterTown = CanEnterTown; + + this.HealthRegen = HealthRegen; + + this.IsDemon = IsDemon; + this.IsLarge = IsLarge; + this.IsSmall = IsSmall; + this.IsFlying = IsFlying; + this.CanOpenDoors = CanOpenDoors; + this.SpawningColumn = SpawningColumn; + this.IsBoss = IsBoss; + this.IsInteractable = IsInteractable; + + this.IsKillable = IsKillable; + this.CanBeConverted = CanBeConverted; + + this.HitClass = HitClass; + + this.HasSpecialEndDeath = HasSpecialEndDeath; + this.DeadCollision = DeadCollision; + + this.CanBeRevivedByOtherMonsters = CanBeRevivedByOtherMonsters; + + this.AppearanceConfig = AppearanceConfig; + + this.CombatConfig = CombatConfig; + + this.NormalDifficultyConfig = NormalDifficultyConfig; + this.NightmareDifficultyConfig = NightmareDifficultyConfig; + this.HellDifficultyConfig = HellDifficultyConfig; + } + + public IEnemyTypeDifficultyConfig GetDifficultyConfig(eDifficulty Difficulty) + { + switch (Difficulty) + { + case eDifficulty.HELL: + return HellDifficultyConfig; + case eDifficulty.NIGHTMARE: + return NightmareDifficultyConfig; + case eDifficulty.NORMAL: + return NormalDifficultyConfig; + default: + return NormalDifficultyConfig; + } + } + } + + public static class EnemyTypeConfigHelper + { + + // (0) Class namco Type Descriptor + // (4) BaseId PopulateId Spawned Beta Code ClientOnly NoMap + // (11) SizeX SizeY Height + // (14) NoOverlays OverlayHeight + // (16) Velocity Run + // (18) CanStealFrom ColdEffect + // (20) Rarity Level Level(N) Level(H) + // (24) MeleeRng MinGrp MaxGrp + // (27) HD TR LG RA LA RH LH SH S1 S2 S3 S4 S5 S6 S7 S8 + // (43) TotalPieces SpawnComponents BaseW + // (46) AIParam1 Comment AIParam2 Comment AIParam3 Comment AIParam4 Comment AIParam5 Comment + // (56) ModeDH ModeN ModeW ModeGH ModeA1 ModeA2 ModeB ModeC + // (64) ModeS1 ModeS2 ModeS3 ModeS4 ModeDD ModeKB ModeSQ ModeRN + // (72) ElMode ElType ElOver ElPct ElMinD ElMaxD ElDur + // (79) MissA1 MissA2 MissS1 MissS2 MissS3 MissS4 MissC MissSQ + // (87) A1Move A2Move S1Move S2Move S3Move S4Move Cmove + // (94) Align + // (95) IsMelee IsSel IsSel2 NeverSel CorpseSel IsAtt IsNPC IsCritter InTown + // (104) Bleed Shadow Light NoUniqueShift CompositeDeath + // (109) Skill1 Skill1Seq Skill1Lvl Skill2 Skill2Seq Skill2Lvl Skill3 Skill3Seq Skill3Lvl Skill4 Skill4Seq Skill4Lvl Skill5 Skill5Seq Skill5Lvl + // (124) LightR LightG LightB + // (127) DamageResist MagicResist FireResist LightResist ColdResist PoisonResist + // (133) DamageResist(N) MagicResist(N) FireResist(N) LightResist(N) ColdResist(N) PoisonResist(N) + // (139) DamageResist(H) MagicResist(H) FireResist(H) LightResist(H) ColdResist(H) PoisonResist(H) + // (145) DamageRegen + // (146) eLUndead eHUndead eDemon eMagicUsing eLarge eSmall eFlying eOpenDoors eSpawnCol eBoss + // (156) PixHeight Interact + // (158) MinHP MaxHP AC Exp ToBlock + // (163) A1MinD A1MaxD A1ToHit A2MinD A2MaxD A2ToHit S1MinD S1MaxD S1ToHit + + + // (172) MinHP(N) MaxHP(N) AC(N) Exp(N) A1MinD(N) A1MaxD(N) A1ToHit(N) A2MinD(N) A2MaxD(N) A2ToHit(N) S1MinD(N) S1MaxD(N) S1ToHit(N) + // (185) MinHP(H) MaxHP(H) AC(H) Exp(H) A1MinD(H) A1MaxD(H) A1ToHit(H) A2MinD(H) A2MaxD(H) A2ToHit(H) S1MinD(H) S1MaxD(H) S1ToHit(H) + // (198) TreasureClass1 TreasureClass2 TreasureClass3 TreasureClass4 + // (202) TreasureClass1(N) TreasureClass2(N) TreasureClass3(N) TreasureClass4(N) + // (206) TreasureClass1(H) TreasureClass2(H) TreasureClass3(H) TreasureClass4(H) + // (210) SpawnPctBonus Soft Heart BodyPart Killable Switch Restore NeverCount HitClass + // (219) SplEndDeath SplGetModeChart SplEndGeneric SplClientEnd + // (223) DeadCollision UnflatDead BloodLocal DeathDamage + // (227) PetIgnore NoGfxHitTest + // (229) HitTestTop HitTestLeft HitTestWidth HitTestHeight + // (233) GenericSpawn AutomapCel SparsePopulate Zoo ObjectCollision Inert + private static int IntConvert(string s) + { + // this is a convenience because sometimes they forget to put a 0 in the D2 monstats.txt + if (string.IsNullOrEmpty(s)) + { + return 0; + } + return Convert.ToInt32(s); + } + + public static IEnemyTypeConfig ToEnemyTypeConfig(this string[] row) + { + return new EnemyTypeConfig( + InternalName: row[0], + Name: row[1], + Type: row[2], + Descriptor: row[3], + + BaseId: IntConvert(row[4]), + PopulateId: IntConvert(row[5]), + Spawned: (row[6] == "1"), + Beta: (row[7] == "1"), + Code: row[8], + ClientOnly: (row[9] == "1"), + NoMap: (row[10] == "1"), + + SizeX: IntConvert(row[11]), + SizeY: IntConvert(row[12]), + Height: IntConvert(row[13]), + NoOverlays: (row[14] == "1"), + OverlayHeight: IntConvert(row[15]), + + Velocity: IntConvert(row[16]), + RunVelocity: IntConvert(row[17]), + + CanStealFrom: (row[18] == "1"), + ColdEffect: IntConvert(row[19]), + + Rarity: (row[20] == "1"), + + + MinimumGrouping: IntConvert(row[25]), + MaximumGrouping: IntConvert(row[26]), + + BaseWeapon: row[45], + + AIParams: new int[] + { + IntConvert(row[46]), + IntConvert(row[48]), + IntConvert(row[50]), + IntConvert(row[52]), + IntConvert(row[54]) + }, + + Allied: (row[94] == "1"), + IsNPC: (row[101] == "1"), + IsCritter: (row[102] == "1"), + CanEnterTown: (row[103] == "1"), + + HealthRegen: IntConvert(row[145]), + + // (146) eLUndead eHUndead eDemon eMagicUsing eLarge eSmall eFlying eOpenDoors eSpawnCol eBoss + IsDemon: (row[148] == "1"), + IsLarge: (row[150] == "1"), + IsSmall: (row[151] == "1"), + IsFlying: (row[152] == "1"), + CanOpenDoors: (row[153] == "1"), + SpawningColumn: IntConvert(row[154]), + IsBoss: (row[155] == "1"), + IsInteractable: (row[157] == "1"), + + IsKillable: (row[215] == "1"), + CanBeConverted: (row[216] == "1"), + + HitClass: IntConvert(row[218]), + + HasSpecialEndDeath: (row[219] == "1"), + DeadCollision: (row[223] == "1"), + + CanBeRevivedByOtherMonsters: (row[236] == "1"), + + // (56) ModeDH ModeN ModeW ModeGH ModeA1 ModeA2 ModeB ModeC + // (64) ModeS1 ModeS2 ModeS3 ModeS4 ModeDD ModeKB ModeSQ ModeRN + // (95) IsMelee IsSel IsSel2 NeverSel CorpseSel IsAtt IsNPC IsCritter InTown + // (104) Bleed Shadow Light NoUniqueShift CompositeDeath + // (124) LightR LightG LightB + AppearanceConfig: new EnemyTypeAppearanceConfig + ( + HasDeathAnimation: (row[56] == "1"), + HasNeutralAnimation: (row[57] == "1"), + HasWalkAnimation: (row[58] == "1"), + HasGetHitAnimation: (row[59] == "1"), + HasAttack1Animation: (row[60] == "1"), + HasAttack2Animation: (row[61] == "1"), + HasBlockAnimation: (row[62] == "1"), + HasCastAnimation: (row[63] == "1"), + HasSkillAnimation: new bool[] + { + (row[64] == "1"), + (row[65] == "1"), + (row[66] == "1"), + (row[67] == "1") + }, + HasCorpseAnimation: (row[68] == "1"), + HasKnockbackAnimation: (row[69] == "1"), + HasRunAnimation: (row[71] == "1"), + + HasLifeBar: (row[96] == "1"), + HasNameBar: (row[97] == "1"), + CannotBeSelected: (row[98] == "1"), + CanCorpseBeSelected: (row[99] == "1"), + + BleedType: IntConvert(row[104]), + HasShadow: (row[105] == "1"), + LightRadius: IntConvert(row[106]), + HasUniqueBossColors: (row[107] == "0"), // note: inverting the bool here + // since in the table it is "NoUniqueShift" but I want to store it as "HasUniqueShift" + CompositeDeath: (row[108] == "1"), + + LightRGB: new byte[] + { + Convert.ToByte(row[124]), + Convert.ToByte(row[125]), + Convert.ToByte(row[126]) + } + ), + + // (72) ElMode ElType ElOver ElPct ElMinD ElMaxD ElDur + // (79) MissA1 MissA2 MissS1 MissS2 MissS3 MissS4 MissC MissSQ + // (87) A1Move A2Move S1Move S2Move S3Move S4Move Cmove + // (95) IsMelee IsSel IsSel2 NeverSel CorpseSel IsAtt IsNPC IsCritter InTown + // (109) Skill1 Skill1Seq Skill1Lvl Skill2 Skill2Seq Skill2Lvl Skill3 Skill3Seq Skill3Lvl Skill4 Skill4Seq Skill4Lvl Skill5 Skill5Seq Skill5Lvl + CombatConfig: new EnemyTypeCombatConfig + ( + ElementalAttackMode: IntConvert(row[72]), + ElementalAttackType: (eDamageTypes)IntConvert(row[73]), + ElementalOverlayId: IntConvert(row[74]), + ElementalChance: IntConvert(row[75]), + ElementalMinDamage: IntConvert(row[76]), + ElementalMaxDamage: IntConvert(row[77]), + ElementalDuration: IntConvert(row[78]), + + MissileForAttack: new int[] + { + IntConvert(row[79]), + IntConvert(row[80]) + }, + MissileForSkill: new int[] + { + IntConvert(row[81]), + IntConvert(row[82]), + IntConvert(row[83]), + IntConvert(row[84]) + }, + MissileForCase: IntConvert(row[85]), + MissileForSequence: IntConvert(row[86]), + + CanMoveAttack: new bool[] + { + (row[87] == "1"), + (row[88] == "1") + }, + CanMoveSkill: new bool[] + { + (row[89] == "1"), + (row[90] == "1"), + (row[91] == "1"), + (row[92] == "1") + }, + CanMoveCast: (row[93] == "1"), + + IsMelee: (row[95] == "1"), + IsAttackable: (row[100] == "1"), + + MeleeRange: IntConvert(row[24]), + + SkillType: new int[] + { + IntConvert(row[109]), + IntConvert(row[112]), + IntConvert(row[115]), + IntConvert(row[118]), + IntConvert(row[121]), + }, + SkillSequence: new int[] + { + IntConvert(row[110]), + IntConvert(row[113]), + IntConvert(row[116]), + IntConvert(row[119]), + IntConvert(row[122]), + }, + SkillLevel: new int[] + { + IntConvert(row[111]), + IntConvert(row[114]), + IntConvert(row[117]), + IntConvert(row[120]), + IntConvert(row[123]), + }, + + IsUndeadWithPhysicalAttacks: (row[146] == "1"), + IsUndeadWithMagicAttacks: (row[147] == "1"), + UsesMagicAttacks: (row[149] == "1"), + + ChanceToBlock: IntConvert(row[162]), + + DoesDeathDamage: (row[226] == "1"), + + IgnoredBySummons: (row[227] == "1") + ), + + // (127) DamageResist MagicResist FireResist LightResist ColdResist PoisonResist + // (133) DamageResist(N) MagicResist(N) FireResist(N) LightResist(N) ColdResist(N) PoisonResist(N) + // (139) DamageResist(H) MagicResist(H) FireResist(H) LightResist(H) ColdResist(H) PoisonResist(H) + // (158) MinHP MaxHP AC Exp ToBlock + // (163) A1MinD A1MaxD A1ToHit A2MinD A2MaxD A2ToHit S1MinD S1MaxD S1ToHit + // (172) MinHP(N) MaxHP(N) AC(N) Exp(N) A1MinD(N) A1MaxD(N) A1ToHit(N) A2MinD(N) A2MaxD(N) A2ToHit(N) S1MinD(N) S1MaxD(N) S1ToHit(N) + // (185) MinHP(H) MaxHP(H) AC(H) Exp(H) A1MinD(H) A1MaxD(H) A1ToHit(H) A2MinD(H) A2MaxD(H) A2ToHit(H) S1MinD(H) S1MaxD(H) S1ToHit(H) + // (192) TreasureClass1 TreasureClass2 TreasureClass3 TreasureClass4 + // (202) TreasureClass1(N) TreasureClass2(N) TreasureClass3(N) TreasureClass4(N) + // (206) TreasureClass1(H) TreasureClass2(H) TreasureClass3(H) TreasureClass4(H) + NormalDifficultyConfig: new EnemyTypeDifficultyConfig + ( + Level: IntConvert(row[21]), + + DamageResist: (IntConvert(row[127]) / 100.0), + MagicResist: (IntConvert(row[128]) / 100.0), + FireResist: (IntConvert(row[129]) / 100.0), + LightResist: (IntConvert(row[130]) / 100.0), + ColdResist: (IntConvert(row[131]) / 100.0), + PoisonResist: (IntConvert(row[132]) / 100.0), + + MinHP: IntConvert(row[158]), + MaxHP: IntConvert(row[159]), + AC: IntConvert(row[160]), + Exp: IntConvert(row[161]), + + AttackMinDamage: new int[] + { + IntConvert(row[163]), + IntConvert(row[166]) + }, + AttackMaxDamage: new int[] + { + IntConvert(row[164]), + IntConvert(row[167]) + }, + AttackChanceToHit: new int[] + { + IntConvert(row[165]), + IntConvert(row[168]) + }, + Skill1MinDamage: IntConvert(row[169]), + Skill1MaxDamage: IntConvert(row[170]), + Skill1ChanceToHit: IntConvert(row[171]), + + TreasureClass: new string[] + { + row[198], row[199], row[200], row[201] + } + ), + NightmareDifficultyConfig: new EnemyTypeDifficultyConfig + ( + Level: IntConvert(row[22]), + + DamageResist: (IntConvert(row[133]) / 100.0), + MagicResist: (IntConvert(row[134]) / 100.0), + FireResist: (IntConvert(row[135]) / 100.0), + LightResist: (IntConvert(row[136]) / 100.0), + ColdResist: (IntConvert(row[137]) / 100.0), + PoisonResist: (IntConvert(row[138]) / 100.0), + + MinHP: IntConvert(row[172]), + MaxHP: IntConvert(row[173]), + AC: IntConvert(row[174]), + Exp: IntConvert(row[175]), + + AttackMinDamage: new int[] + { + IntConvert(row[176]), + IntConvert(row[179]) + }, + AttackMaxDamage: new int[] + { + IntConvert(row[177]), + IntConvert(row[180]) + }, + AttackChanceToHit: new int[] + { + IntConvert(row[178]), + IntConvert(row[181]) + }, + Skill1MinDamage: IntConvert(row[182]), + Skill1MaxDamage: IntConvert(row[183]), + Skill1ChanceToHit: IntConvert(row[184]), + + TreasureClass: new string[] + { + row[202], row[203], row[204], row[205] + } + ), + HellDifficultyConfig: new EnemyTypeDifficultyConfig + ( + Level: IntConvert(row[23]), + + DamageResist: (IntConvert(row[139]) / 100.0), + MagicResist: (IntConvert(row[140]) / 100.0), + FireResist: (IntConvert(row[141]) / 100.0), + LightResist: (IntConvert(row[142]) / 100.0), + ColdResist: (IntConvert(row[143]) / 100.0), + PoisonResist: (IntConvert(row[144]) / 100.0), + + MinHP: IntConvert(row[185]), + MaxHP: IntConvert(row[186]), + AC: IntConvert(row[187]), + Exp: IntConvert(row[188]), + + AttackMinDamage: new int[] + { + IntConvert(row[189]), + IntConvert(row[192]) + }, + AttackMaxDamage: new int[] + { + IntConvert(row[190]), + IntConvert(row[193]) + }, + AttackChanceToHit: new int[] + { + IntConvert(row[191]), + IntConvert(row[194]) + }, + Skill1MinDamage: IntConvert(row[195]), + Skill1MaxDamage: IntConvert(row[196]), + Skill1ChanceToHit: IntConvert(row[197]), + + TreasureClass: new string[] + { + row[206], row[207], row[208], row[209] + } + ) + ); + } + } +} diff --git a/OpenDiablo2.Common/Models/Mobs/EnemyTypeDifficultyConfig.cs b/OpenDiablo2.Common/Models/Mobs/EnemyTypeDifficultyConfig.cs new file mode 100644 index 00000000..f0cbbcd1 --- /dev/null +++ b/OpenDiablo2.Common/Models/Mobs/EnemyTypeDifficultyConfig.cs @@ -0,0 +1,68 @@ +using OpenDiablo2.Common.Interfaces.Mobs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Models.Mobs +{ + public class EnemyTypeDifficultyConfig : IEnemyTypeDifficultyConfig + { + + public int Level { get; private set; } + + public double DamageResist { get; private set; } + public double MagicResist { get; private set; } + public double FireResist { get; private set; } + public double LightningResist { get; private set; } + public double ColdResist { get; private set; } + public double PoisonResist { get; private set; } + + public int MinHP { get; private set; } + public int MaxHP { get; private set; } + public int AC { get; private set; } // armor class + public int Exp { get; private set; } + + public int[] AttackMinDamage { get; private set; } // 1-2, min damage attack can roll + public int[] AttackMaxDamage { get; private set; } // 1-2 max damage attack can roll + public int[] AttackChanceToHit { get; private set; } // 1-2 chance attack has to hit (out of 100??) + public int Skill1MinDamage { get; private set; } // min damage skill 1 can do (why only skill 1?) + public int Skill1MaxDamage { get; private set; } // max damage for skill 1 + public int Skill1ChanceToHit { get; private set; } // chance skill has to hit + + public string[] TreasureClass { get; private set; } // 1-4 + + public EnemyTypeDifficultyConfig (int Level, + double DamageResist, double MagicResist, double FireResist, double LightResist, + double ColdResist, double PoisonResist, + int MinHP, int MaxHP, int AC, int Exp, + int[] AttackMinDamage, int[] AttackMaxDamage, int[] AttackChanceToHit, + int Skill1MinDamage, int Skill1MaxDamage, int Skill1ChanceToHit, + string[] TreasureClass) + { + this.Level = Level; + + this.DamageResist = DamageResist; + this.MagicResist = MagicResist; + this.FireResist = FireResist; + this.LightningResist = LightResist; + this.ColdResist = ColdResist; + this.PoisonResist = PoisonResist; + + this.MinHP = MinHP; + this.MaxHP = MaxHP; + this.AC = AC; + this.Exp = Exp; + + this.AttackMinDamage = AttackMinDamage; + this.AttackMaxDamage = AttackMaxDamage; + this.AttackChanceToHit = AttackChanceToHit; + this.Skill1MinDamage = Skill1MinDamage; + this.Skill1MaxDamage = Skill1MaxDamage; + this.Skill1ChanceToHit = Skill1ChanceToHit; + + this.TreasureClass = TreasureClass; + } + } +} diff --git a/OpenDiablo2.Common/OpenDiablo2.Common.csproj b/OpenDiablo2.Common/OpenDiablo2.Common.csproj index 568760d2..38630911 100644 --- a/OpenDiablo2.Common/OpenDiablo2.Common.csproj +++ b/OpenDiablo2.Common/OpenDiablo2.Common.csproj @@ -61,6 +61,7 @@ + @@ -98,6 +99,14 @@ + + + + + + + + diff --git a/OpenDiablo2.Common/ResourcePaths.cs b/OpenDiablo2.Common/ResourcePaths.cs index 83ddde0d..cba6fdd5 100644 --- a/OpenDiablo2.Common/ResourcePaths.cs +++ b/OpenDiablo2.Common/ResourcePaths.cs @@ -1,4 +1,4 @@ -/* OpenDiablo 2 - An open source re-implementation of Diablo 2 in C# +/* OpenDiablo 2 - An open source re-implementation of Diablo 2 in C# * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -219,6 +219,9 @@ namespace OpenDiablo2.Common public const string SFXSorceressDeselect = @"data\global\sfx\Cursor\intro\sorceress deselect.wav"; public const string SFXSorceressSelect = @"data\global\sfx\Cursor\intro\sorceress select.wav"; + // --- Enemy Data --- + public static string MonStats = "data\\global\\excel\\monstats.txt"; + public static string GeneratePathForItem(string spriteName) { return $@"data\global\items\{spriteName}.dc6"; @@ -304,3 +307,4 @@ namespace OpenDiablo2.Common } } } + diff --git a/OpenDiablo2.Core/EngineDataManager.cs b/OpenDiablo2.Core/EngineDataManager.cs index bd503415..d8f623de 100644 --- a/OpenDiablo2.Core/EngineDataManager.cs +++ b/OpenDiablo2.Core/EngineDataManager.cs @@ -26,6 +26,7 @@ namespace OpenDiablo2.Core public ImmutableList Items { get; internal set; } public ImmutableDictionary ExperienceConfigs { get; internal set; } public ImmutableDictionary HeroTypeConfigs { get; internal set; } + public ImmutableList EnemyTypeConfigs { get; internal set; } public EngineDataManager(IMPQProvider mpqProvider) { @@ -33,6 +34,7 @@ namespace OpenDiablo2.Core LoadLevelDetails(); LoadCharacterData(); + LoadEnemyData(); Items = LoadItemData(); } @@ -139,6 +141,23 @@ namespace OpenDiablo2.Core .Where(x => !String.IsNullOrWhiteSpace(x)) .Select(x => x.Split('\t')) .Where(x => x[0] != "Expansion") + .ToArray() .ToImmutableDictionary(x => (eHero)Enum.Parse(typeof(eHero), x[0]), x => x.ToHeroTypeConfig()); + + private void LoadEnemyData() + { + EnemyTypeConfigs = LoadEnemyTypeConfig(); + } + + private ImmutableList LoadEnemyTypeConfig() + => mpqProvider + .GetTextFile(ResourcePaths.MonStats) + .Skip(1) + .Where(x => !String.IsNullOrWhiteSpace(x)) + .Select(x => x.Split('\t')) + .Where(x => x[0] != "Expansion" && x[0] != "unused") + .ToArray() + .Select(x => x.ToEnemyTypeConfig()) + .ToImmutableList(); } }