Enemy.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. using Microsoft.Xna.Framework;
  2. using OpenVIII.Battle.Dat;
  3. using OpenVIII.Kernel;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.IO;
  7. using System.Linq;
  8. namespace OpenVIII
  9. {
  10. public class Enemy : Damageable, IEnemy
  11. {
  12. #region Fields
  13. private const int StatusDefault = 100;
  14. private bool _mugged;
  15. #endregion Fields
  16. #region Constructors
  17. private Enemy()
  18. {
  19. }
  20. #endregion Constructors
  21. #region Properties
  22. public static List<Enemy> Party { get; set; }
  23. public Battle.Dat.Abilities[] Abilities => HML(Info.AbilitiesHigh, Info.AbilitiesMed, Info.AbilitiesLow);
  24. public byte AP => Info.AP;
  25. public Devour Devour => Info.Devour[LevelGroup()] >= Memory.KernelBin.Devour.Count ?
  26. Memory.KernelBin.Devour[Memory.KernelBin.Devour.Count - 1] :
  27. Memory.KernelBin.Devour[Info.Devour[LevelGroup()]];
  28. public Magic[] DrawList => HML(Info.DrawHigh, Info.DrawMed, Info.DrawLow);
  29. /// <summary>
  30. /// Randomly gain 1 or 0 from this list.
  31. /// </summary>
  32. public Saves.Item[] DropList => HML(Info.DropHigh, Info.DropMed, Info.DropLow);
  33. public byte DropRate => (byte)(MathHelper.Clamp(Info.DropRate * 100 / byte.MaxValue, 0, 100));
  34. public Battle.EnemyInstanceInformation EII { get; set; }
  35. public IEnumerable<EnemyAttacksData> EnemyAttacksDatas => Abilities.Where(x => x.Monster != null).Select(x => x.Monster);
  36. public override byte EVA => Convert2(Info.EVA);
  37. /// <summary>
  38. /// The EXP everyone gets.
  39. /// </summary>
  40. public override int EXP => Convert3(Info.Exp, Memory.State.AveragePartyLevel);
  41. public byte FixedLevel { get; set; }
  42. /// <summary>
  43. /// Enemy attacks determine hit%
  44. /// </summary>
  45. /// <remarks>
  46. /// LeythalKnight Today at 1:53 PM Enemy attack accuracy is set per ability based on the value
  47. /// that's called "Attack param" in Doomtrain, and only when they use attack types that have
  48. /// a miss chance
  49. /// </remarks>
  50. public override byte HIT => 0;
  51. public Information Info => EII.Data.Information;
  52. public IEnumerable<GFs> JunctionedGFs => Memory.State != null ? DrawList.Select(x => x.GF).Where(gf => gf >= GFs.Quezacotl && gf <= GFs.Eden && !Memory.State.UnlockedGFs.Contains(gf)).Distinct() : null;
  53. /// <summary>
  54. /// Level of enemy based on average of party or fixed value.
  55. /// </summary>
  56. /// <see cref="https://finalfantasy.fandom.com/wiki/Level#Enemy_levels"/>
  57. public override byte Level
  58. {
  59. get
  60. {
  61. if (FixedLevel != default)
  62. return FixedLevel;
  63. var a = Memory.State.AveragePartyLevel;
  64. var d = (byte)(a / 5);
  65. return (byte)MathHelper.Clamp(a + d, 1, 100);
  66. }
  67. }
  68. /// <summary>
  69. /// unknown luck stat. putting 0 in here so there is something.
  70. /// </summary>
  71. public override byte LUCK => 0;
  72. public override byte MAG => Convert1(Info.MAG);
  73. /// <summary>
  74. /// Randomly gain 1 or 0 from this list.
  75. /// </summary>
  76. public Saves.Item[] MugList => HML(Info.MugHigh, Info.MugMed, Info.MugLow);
  77. public byte MugRate => (byte)(MathHelper.Clamp(Info.MugRate * 100 / byte.MaxValue, 0, 100));
  78. public override FF8String Name => Info.Name;
  79. public override byte SPD => Convert2(Info.SPD);
  80. public override byte SPR => Convert2(Info.SPR);
  81. public override byte STR => Convert1(Info.STR);
  82. public override byte VIT => Convert2(Info.VIT);
  83. #endregion Properties
  84. #region Methods
  85. public static implicit operator Battle.EnemyInstanceInformation(Enemy @in) => @in.EII;
  86. public static implicit operator Enemy(Battle.EnemyInstanceInformation @in) => Load(@in);
  87. public static Enemy Load(Battle.EnemyInstanceInformation eii, byte fixedLevel = 0, ushort? startingHP = null)
  88. {
  89. var r = new Enemy
  90. {
  91. EII = eii,
  92. FixedLevel = fixedLevel
  93. };
  94. r._CurrentHP = startingHP ?? r.MaxHP();
  95. if ((r.Info.BitSwitch & Flag1.Zombie) != 0)
  96. {
  97. r.Statuses0 |= PersistentStatuses.Zombie;
  98. }
  99. if ((r.Info.BitSwitch & Flag1.AutoProtect) != 0)
  100. {
  101. r.Statuses1 |= BattleOnlyStatuses.Protect;
  102. }
  103. if ((r.Info.BitSwitch & Flag1.AutoReflect) != 0)
  104. {
  105. r.Statuses1 |= BattleOnlyStatuses.Reflect;
  106. }
  107. if ((r.Info.BitSwitch & Flag1.AutoShell) != 0)
  108. {
  109. r.Statuses1 |= BattleOnlyStatuses.Shell;
  110. }
  111. if ((r.Info.BitSwitch & Flag1.Fly) != 0)
  112. {
  113. r.Statuses1 |= BattleOnlyStatuses.Float;
  114. }
  115. r.Init();
  116. return r;
  117. }
  118. /// <summary>
  119. /// Return card if succeed at roll
  120. /// </summary>
  121. /// <returns></returns>
  122. /// <see cref="https://gamefaqs.gamespot.com/ps/197343-final-fantasy-viii/faqs/58936"/>
  123. public Cards.ID Card()
  124. {
  125. if (Info.Card.Skip(1).All(x => x == Cards.ID.Immune)) return Cards.ID.Immune;
  126. var p = (256 * MaxHP() - 255 * CurrentHP()) / MaxHP();
  127. var r = Memory.Random.Next(256);
  128. // 2 is rare card, 1 is normal card 0 per ifrit.
  129. return r < (p + 1) ? (r < 17 ? Info.Card[2] : Info.Card[1]) : Cards.ID.Fail;
  130. }
  131. public Cards.ID CardDrop()
  132. {
  133. //9 / 256
  134. if (Info.Card[0] == Cards.ID.Immune) return Info.Card[1];
  135. var r = Memory.Random.Next(256);
  136. return r < 9 ? Info.Card[1] : Cards.ID.Fail;
  137. }
  138. public override Damageable Clone() => throw new NotImplementedException();
  139. public Saves.Item Drop(bool rareItem = false)
  140. {
  141. if (_mugged) return default;
  142. int percent = DropRate;
  143. var list = DropList;
  144. var i = Memory.Random.Next(100 + 1);
  145. if (i >= percent || list.Length <= 0) return default;
  146. //Slot | 0 | 1 | 2 | 3
  147. //------------------| ---------| --------- | ---------| --------
  148. //Without Rare Item | 178 / 256 | 51 / 256 | 15 / 256 | 12 / 256
  149. //------------------| ---------| --------- | ---------| --------
  150. //With Rare Item | 128 / 256 | 114 / 256 | 14 / 256 | 0 / 256 <- kinda makes no scene to me
  151. var r = Memory.Random.Next(256);
  152. if (rareItem)
  153. {
  154. if (r < 128)
  155. return list[0];
  156. if ((r -= 128) < 114)
  157. return list[1];
  158. return (r - 114) < 14 ? list[2] : list[3];
  159. }
  160. if (r < 178)
  161. return list[0];
  162. if ((r -= 178) < 51)
  163. return list[1];
  164. return (r - 51) < 15 ? list[2] : list[3];
  165. }
  166. public override short ElementalResistance(Element @in)
  167. {
  168. var l = (Enum.GetValues(typeof(Element))).Cast<Element>().ToList();
  169. return @in == Element.NonElemental
  170. ? (short)100
  171. : conv(Info.Resistance[l.FindIndex(x => (x & @in) != 0) - 1]);
  172. short conv(byte val) => (short)MathHelper.Clamp(900 - (val * 10), -100, 400);
  173. }
  174. /// <summary>
  175. /// The character whom lands the last hit gets a little bonus xp.
  176. /// </summary>
  177. /// <param name="lastHitLevel">Level of character whom got last hit.</param>
  178. /// <returns></returns>
  179. public int EXPExtra(byte lastHitLevel) => Convert3(Info.ExpExtra, lastHitLevel);
  180. public override ushort MaxHP()
  181. {
  182. //from Ifrit's help file
  183. if (Info.HP == null)
  184. return 0;
  185. var i = (Info.HP[0] * Level * Level / 20) + (Info.HP[0] + Info.HP[2] * 100) * Level + Info.HP[1] * 10 + Info.HP[3] * 1000;
  186. return (ushort)MathHelper.Clamp(i, 0, ushort.MaxValue);
  187. }
  188. public Saves.Item Mug(byte spd, bool rareItem = false)
  189. {
  190. if (_mugged) return default;
  191. var percent = (MugRate + spd);
  192. var list = DropList;
  193. var i = Memory.Random.Next(100 + 1);
  194. try
  195. {
  196. if (i < percent && list.Length > 0)
  197. {
  198. var r = (byte)Memory.Random.Next(256);
  199. if (rareItem)
  200. {
  201. if (r < 128)
  202. return list[0];
  203. else if ((r -= 128) < 114)
  204. return list[1];
  205. else if ((r - 114) < 14)
  206. return list[2];
  207. else
  208. return list[3];
  209. }
  210. if (r < 178)
  211. return list[0];
  212. else if ((r -= 178) < 51)
  213. return list[1];
  214. else if ((r - 51) < 15)
  215. return list[2];
  216. else
  217. return list[3];
  218. }
  219. }
  220. finally { _mugged = true; }
  221. _mugged = false;
  222. return default;
  223. }
  224. /// <summary>
  225. /// I notice that the resistance reported on the wiki is 100 less than the number in the data.
  226. /// </summary>
  227. /// <param name="s">status effect</param>
  228. /// <returns>percent of resistance</returns>
  229. /// <see cref="https://finalfantasy.fandom.com/wiki/G-Soldier#Stats"/>
  230. public override sbyte StatusResistance(PersistentStatuses s)
  231. {
  232. byte r = 100;
  233. switch (s)
  234. {
  235. case PersistentStatuses.Death:
  236. r = Info.DeathResistanceMental;
  237. break;
  238. case PersistentStatuses.Poison:
  239. r = Info.PoisonResistanceMental;
  240. break;
  241. case PersistentStatuses.Petrify:
  242. r = Info.PetrifyResistanceMental;
  243. break;
  244. case PersistentStatuses.Darkness:
  245. r = Info.DarknessResistanceMental;
  246. break;
  247. case PersistentStatuses.Silence:
  248. r = Info.SilenceResistanceMental;
  249. break;
  250. case PersistentStatuses.Berserk:
  251. r = Info.BerserkResistanceMental;
  252. break;
  253. case PersistentStatuses.Zombie:
  254. r = Info.ZombieResistanceMental;
  255. break;
  256. }
  257. return (sbyte)MathHelper.Clamp(r - 100, -100, 100);
  258. }
  259. public override sbyte StatusResistance(BattleOnlyStatuses s)
  260. {
  261. byte r = StatusDefault;
  262. switch (s)
  263. {
  264. case BattleOnlyStatuses.Sleep:
  265. r = Info.SleepResistanceMental;
  266. break;
  267. case BattleOnlyStatuses.Haste:
  268. r = Info.HasteResistanceMental;
  269. break;
  270. case BattleOnlyStatuses.Slow:
  271. r = Info.SlowResistanceMental;
  272. break;
  273. case BattleOnlyStatuses.Stop:
  274. r = Info.StopResistanceMental;
  275. break;
  276. case BattleOnlyStatuses.Regen:
  277. r = Info.RegenResistanceMental;
  278. break;
  279. case BattleOnlyStatuses.Protect:
  280. break;
  281. case BattleOnlyStatuses.Shell:
  282. break;
  283. case BattleOnlyStatuses.Reflect:
  284. r = Info.ReflectResistanceMental;
  285. break;
  286. case BattleOnlyStatuses.Aura:
  287. break;
  288. case BattleOnlyStatuses.Curse:
  289. break;
  290. case BattleOnlyStatuses.Doom:
  291. r = Info.DoomResistanceMental;
  292. break;
  293. case BattleOnlyStatuses.Invincible:
  294. break;
  295. case BattleOnlyStatuses.Petrifying:
  296. r = Info.SlowPetrifyResistanceMental;
  297. break;
  298. case BattleOnlyStatuses.Float:
  299. r = Info.FloatResistanceMental;
  300. break;
  301. case BattleOnlyStatuses.Confuse:
  302. r = Info.ConfuseResistanceMental;
  303. break;
  304. case BattleOnlyStatuses.Drain:
  305. r = Info.DrainResistanceMental;
  306. break;
  307. case BattleOnlyStatuses.Eject:
  308. r = Info.ExpulsionResistanceMental;
  309. break;
  310. case BattleOnlyStatuses.None:
  311. break;
  312. case BattleOnlyStatuses.Double:
  313. break;
  314. case BattleOnlyStatuses.Triple:
  315. break;
  316. case BattleOnlyStatuses.Defend:
  317. break;
  318. case BattleOnlyStatuses.Unk0X100000:
  319. break;
  320. case BattleOnlyStatuses.Unk0X200000:
  321. break;
  322. case BattleOnlyStatuses.Charged:
  323. break;
  324. case BattleOnlyStatuses.BackAttack:
  325. break;
  326. case BattleOnlyStatuses.Vit0:
  327. break;
  328. case BattleOnlyStatuses.AngelWing:
  329. break;
  330. case BattleOnlyStatuses.Unk0X4000000:
  331. break;
  332. case BattleOnlyStatuses.Unk0X8000000:
  333. break;
  334. case BattleOnlyStatuses.Unk0X10000000:
  335. break;
  336. case BattleOnlyStatuses.Unk0X20000000:
  337. break;
  338. case BattleOnlyStatuses.HasMagic:
  339. break;
  340. case BattleOnlyStatuses.SummonGF:
  341. break;
  342. default:
  343. throw new ArgumentOutOfRangeException(nameof(s), s, null);
  344. }
  345. return (sbyte)MathHelper.Clamp(r - 100, -100, 100);
  346. }
  347. /// <summary>
  348. /// I notice that the resistance reported on the wiki is 100 less than the number in the data.
  349. /// </summary>
  350. /// <param name="s">status effect</param>
  351. /// <returns>percent of resistance</returns>
  352. /// <see cref="https://finalfantasy.fandom.com/wiki/G-Soldier#Stats"/>
  353. /// <summary>
  354. /// The wiki says some areas have forced or random levels. This lets you override the level.
  355. /// </summary>
  356. /// <see cref="https://finalfantasy.fandom.com/wiki/Level#Enemy_levels"/>
  357. public override string ToString() => Name.Value_str;
  358. public override ushort TotalStat(Stat s)
  359. {
  360. switch (s)
  361. {
  362. case Stat.HP:
  363. return CurrentHP();
  364. case Stat.EVA:
  365. //TODO confirm if there is no flat stat buff for eva. If there isn't then remove from function.
  366. return EVA;
  367. case Stat.SPD:
  368. return SPD;
  369. case Stat.HIT:
  370. return HIT;
  371. case Stat.Luck:
  372. return LUCK;
  373. case Stat.MAG:
  374. return MAG;
  375. case Stat.SPR:
  376. return SPR;
  377. case Stat.STR:
  378. return STR;
  379. case Stat.VIT:
  380. return VIT;
  381. case Stat.ElAtk:
  382. break;
  383. case Stat.StAtk:
  384. break;
  385. case Stat.ElDef1:
  386. break;
  387. case Stat.ElDef2:
  388. break;
  389. case Stat.ElDef3:
  390. break;
  391. case Stat.ElDef4:
  392. break;
  393. case Stat.StDef1:
  394. break;
  395. case Stat.StDef2:
  396. break;
  397. case Stat.StDef3:
  398. break;
  399. case Stat.StDef4:
  400. break;
  401. case Stat.None:
  402. break;
  403. default:
  404. throw new ArgumentOutOfRangeException(nameof(s), s, null);
  405. }
  406. return 0;
  407. }
  408. protected override void ReadData(BinaryReader br, Enum @enum) => throw new NotImplementedException("This method is not used by Enemy");
  409. private byte Convert1(IReadOnlyList<byte> @in)
  410. {
  411. //from Ifrit's help file
  412. var level = Level;
  413. var i = level * @in[0] / 10 + level / @in[1] - level * level / 2 / (@in[3] + @in[2]) / 4;
  414. //PLEASE NOTE: I'm not 100% sure on the STR/MAG formula, but it should be accurate enough to get the general idea.
  415. // wiki states something like ([3(Lv)] + [(Lv) / 5] - [(Lv)² / 260] + 12) / 4
  416. return (byte)MathHelper.Clamp(i, 0, byte.MaxValue);
  417. }
  418. private byte Convert2(IReadOnlyList<byte> @in)
  419. {
  420. //from Ifrit's help file
  421. var level = Level;
  422. var i = level / @in[1] - level / @in[3] + level * @in[0] + @in[2];
  423. return (byte)MathHelper.Clamp(i, 0, byte.MaxValue);
  424. }
  425. private int Convert3(ushort @in, byte inLevel)
  426. {
  427. //from Ifrit's help file
  428. var level = Level;
  429. if (inLevel == 0)
  430. return 0;
  431. return @in * (5 * (level - inLevel) / inLevel + 4);
  432. }
  433. private T HML<T>(T h, T m, T l)
  434. {
  435. var level = Level;
  436. if (level > Info.HighLevelStart)
  437. return h;
  438. if (level > Info.MedLevelStart)
  439. return m;
  440. return l;
  441. }
  442. private int LevelGroup()
  443. {
  444. var l = Level;
  445. if (l > Info.HighLevelStart)
  446. return 2;
  447. return l > Info.MedLevelStart ? 1 : 0;
  448. }
  449. #endregion Methods
  450. }
  451. }