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();
}
}