diff --git a/FF1Blazorizer/Tabs/ItemEquipmentTab.razor b/FF1Blazorizer/Tabs/ItemEquipmentTab.razor index 101a438a2..bbd46026b 100644 --- a/FF1Blazorizer/Tabs/ItemEquipmentTab.razor +++ b/FF1Blazorizer/Tabs/ItemEquipmentTab.razor @@ -17,8 +17,8 @@ Dragonslayer Legendary Kit Random Endgame Weapon - Random AoE Caster Item - Random Caster Item + Random AoE Caster Item + Random Caster Item One Legendary/Caster Item Random Typed Weapon Grandpa's Secret Stash diff --git a/FF1Lib/Helpers/SpellHelper.cs b/FF1Lib/Helpers/SpellHelper.cs index 099a8521e..d784e3505 100644 --- a/FF1Lib/Helpers/SpellHelper.cs +++ b/FF1Lib/Helpers/SpellHelper.cs @@ -62,5 +62,14 @@ public SpellHelper(List _spellInfos) //yeah stupid way of doing it, but i don't care return SpellInfos.Select(s => ((Spell)Convert.ToByte((int)Spell.CURE + s.Index), s)).ToList(); } + + public IEnumerable<(Spell Id, MagicSpell Info)> GetAoEAttackSpells() + { + var damageAoes = FindSpells(SpellRoutine.Damage, SpellTargeting.AllEnemies); + var instaAoes = FindSpells(SpellRoutine.InflictStatus, SpellTargeting.AllEnemies, SpellElement.Any, SpellStatus.Death); + var powerWordAoes = FindSpells(SpellRoutine.PowerWord, SpellTargeting.AllEnemies, SpellElement.Any, SpellStatus.Death); + + return damageAoes.Concat(instaAoes).Concat(powerWordAoes); + } } } diff --git a/FF1Lib/ItemMagic.cs b/FF1Lib/ItemMagic.cs index 88b6c2c06..6e2dcf6a0 100644 --- a/FF1Lib/ItemMagic.cs +++ b/FF1Lib/ItemMagic.cs @@ -82,6 +82,16 @@ public void ShuffleItemMagic(MT19337 rng, Flags flags) Spells = GetAllSpells(rng); } + if ((bool)flags.StartingEquipmentRandomAoe && flags.ItemMagicMode != ItemMagicMode.None) { + // guarantee that at least one item has an AoE attack spell + if (!Spells.Any(spell => spell.IsAoEAttack())) { + var spellHelper = new SpellHelper(this); + var aoe_spells = new List(spellHelper.GetAoEAttackSpells().Select(s => s.Info).ToList()); + aoe_spells.Shuffle(rng); + Spells.Add(aoe_spells.First()); + } + } + foreach (var item in Spells.Zip(magicItems, (s, i) => new { Spell = s, Item = i })) { WriteItemSpellData(item.Spell, item.Item); diff --git a/FF1Lib/Magic.cs b/FF1Lib/Magic.cs index c4491fed5..de256516d 100644 --- a/FF1Lib/Magic.cs +++ b/FF1Lib/Magic.cs @@ -1,1242 +1,1258 @@ -using System.ComponentModel; -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using FF1Lib.Helpers; - -namespace FF1Lib -{ - - public enum SpellRoutine : byte - { - None = 0, - Damage = 0x01, - DamageUndead = 0x02, - Heal = 0x07, - CureAilment = 0x08, - FullHeal = 0x0F, - ArmorUp = 0x09, - DefElement = 0x0A, - Fast = 0x0C, - Sabr = 0x0D, - Lock = 0x0E, - Ruse = 0x10, - PowerWord = 0x12, - InflictStatus = 0x03, - Life = 0xF0, - Smoke = 0xF1, - Slow = 0x04, - Fear = 0x05, - Xfer = 0x11, - } - - public enum SpellTargeting : byte - { - Any = 0xFF, - None = 0, - AllEnemies = 0x01, - OneEnemy = 0x02, - Self = 0x04, - AllCharacters = 0x08, - OneCharacter = 0x10 - } - - [Flags] - public enum SpellElement : byte - { - Any = 0b10101010, - None = 0x00, - Earth = 0b10000000, - Lightning = 0b01000000, - Ice = 0b00100000, - Fire = 0b00010000, - Death = 0b00001000, - Time = 0b00000100, - Poison = 0b00000010, - Status = 0b00000001, - All = 0xFF - } - - [Flags] - public enum SpellStatus : byte - { - None = 0, - Any = 0xFF, - Confuse = 0b10000000, - Mute = 0b01000000, - Dark = 0b00001000, - Stun = 0b00010000, - Sleep = 0b00100000, - Stone = 0b00000010, - Death = 0b00000001, - Poison = 0b00000100 - } - - public enum OOBSpellRoutine : byte { - CURE = 0, - CUR2 = 1, - CUR3 = 2, - CUR4 = 3, - HEAL = 4, - HEL3 = 5, - HEL2 = 6, - PURE = 7, - LIFE = 8, - LIF2 = 9, - WARP = 10, - SOFT = 11, - EXIT = 12, - None = 255 - } - - public enum MagicGraphic : byte { - None = 0, - BarOfLight = 176, - FourSparkles = 184, - Stars = 192, - EnergyBeam = 200, - EnergyFlare = 208, - GlowingBall = 216, - LargeSparkle = 224, - SparklingHand = 232 - } - - public enum SpellColor : byte { - White = 0x20, - Blue = 0x21, - Violet = 0x22, - Purple = 0x23, - Pink = 0x24, - PinkOrange = 0x25, - LightOrange = 0x26, - DarkOrange = 0x27, - Yellow = 0x28, - Green = 0x29, - LightGreen = 0x2A, - BlueGreen = 0x2B, - Teal = 0x2C, - Gray = 0x2D, - Black1 = 0x2E, - Black2 = 0x2F - } - - public enum SpellSchools - { - White = 0, - Black - } - - [JsonObject(MemberSerialization.OptIn)] - public class MagicSpell - { - [JsonProperty] - public byte Index; - - public bool ShouldSerializeIndex() { - return !isRegularSpell; - } - - public Blob Data; - - [JsonProperty] - public string Name; - - [JsonProperty] - public byte TextPointer; - - public bool ShouldSerializeTextPointer() { - return isRegularSpell; - } - - [JsonProperty] - public string Message; - - public bool ShouldSerializeMessage() { - return isRegularSpell; - } - - [JsonProperty] - public byte accuracy = 0; - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public SpellElement elem = 0; - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public SpellTargeting targeting = 0; - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public SpellRoutine routine = 0; - - [JsonProperty] - public byte effect = 0; - - public bool ShouldSerializeeffect() { - return routine != SpellRoutine.CureAilment && - routine != SpellRoutine.InflictStatus && - routine != SpellRoutine.DefElement && - routine != SpellRoutine.Slow && - routine != SpellRoutine.None && - routine != SpellRoutine.FullHeal && - routine != SpellRoutine.Xfer; - - } - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public SpellStatus status { - get { return (SpellStatus)effect; } - set { effect = (byte)value; } - } - public bool ShouldSerializestatus() { - return routine == SpellRoutine.CureAilment || routine == SpellRoutine.InflictStatus; - } - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public SpellElement defelement { - get { - return (SpellElement)effect; - } - set { - effect = (byte)value; - } - } - public bool ShouldSerializedefelement() { - return routine == SpellRoutine.DefElement; - } - - public byte gfx = 0; - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public MagicGraphic MagicGraphic { - get { - return (MagicGraphic)gfx; - } - set { - gfx = (byte)value; - } - } - - public bool ShouldSerializegfx() { - return isRegularSpell; - } - - public byte palette = 0; - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public SpellColor SpellColor { - get { - return (SpellColor)palette; - } - set { - palette = (byte)value; - } - } - - public bool ShouldSerializeSpellColor() { - return isRegularSpell; - } - - void updateMagicIndex(byte level, byte slot, SpellSchools type) { - this.Index = (byte)((level-1) * 8 + (slot-1)); - if (type == SpellSchools.Black) { - this.Index += 4; - } - } - - [JsonProperty] - public byte Level { - get { - return (byte)((Index / 8)+1); - } - set { - if (value < 1 || value > 8) { - throw new Exception("Spell level must be between 1 and 8"); - } - this.updateMagicIndex(value, Slot, SpellSchool); - } - } - - public bool ShouldSerializeLevel() { - return isRegularSpell; - } - - [JsonProperty] - public byte Slot { - get { - return (byte)((Index % 4) + 1); - } - set { - if (value < 1 || value > 4) { - throw new Exception("Spell slot must be between 1 and 4"); - } - this.updateMagicIndex(Level, value, SpellSchool); - } - } - - public bool ShouldSerializeSlot() { - return isRegularSpell; - } - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public SpellSchools SpellSchool { - get { - if (Index % 8 < 4) { - return SpellSchools.White; - } - return SpellSchools.Black; - } - set { - this.updateMagicIndex(Level, Slot, value); - } - } - - public bool ShouldSerializeMagicType() { - return isRegularSpell; - } - - [JsonProperty] - [JsonConverter(typeof(StringEnumConverter))] - public OOBSpellRoutine oobSpellRoutine = OOBSpellRoutine.None; - - public bool ShouldSerializeoobSpellRoutine() { - return isRegularSpell; - } - - List _permissions = new(); - - [JsonProperty] - public string permissions { - get { - if (_permissions == null) { - return ""; - } - string ret = ""; - foreach (var c in _permissions) { - if (ret != "") { - ret += ", "; - } - ret += Enum.GetName(c); - } - return ret; - } - set { - _permissions.Clear(); - var sp = value.Split(","); - foreach (var cl in sp) { - _permissions.Add(Enum.Parse(cl)); - } - } - } - - public bool ShouldSerializepermissions() { - return isRegularSpell; - } - - bool isRegularSpell; - - public MagicSpell(byte _Index, - Blob _Data, - string _Name, - byte _TextPointer, - string _Message, - List __permissions) - { - Index = _Index; - Data = _Data; - Name = _Name; - TextPointer = _TextPointer; - Message = _Message; - _permissions = __permissions; - isRegularSpell = true; - this.decompressData(Data); - } - - public MagicSpell(byte _Index, - Blob _Data, - string _Name, - bool _isRegularSpell) - { - Index = _Index; - Data = _Data; - Name = _Name; - isRegularSpell = _isRegularSpell; - this.decompressData(Data); - } - - public MagicSpell() - { - Data = (Blob)new byte[8]; - isRegularSpell = true; - } - - public byte[] compressData() - { - Data[0] = accuracy; - Data[1] = effect; - Data[2] = (byte)elem; - Data[3] = (byte)targeting; - Data[4] = (byte)routine; - Data[5] = gfx; - Data[6] = palette; - Data[7] = 0x00; // last byte is always 00 - return Data; - } - - public void writeData(FF1Rom rom) { - compressData(); - if (isRegularSpell) { - rom.Put(FF1Rom.MagicOffset + FF1Rom.MagicSize * Index, Data); - rom.ItemsText[176 + Index] = Name; - } - } - - public void decompressData(byte[] data) - { - accuracy = data[0]; - effect = data[1]; - elem = (SpellElement)data[2]; - targeting = (SpellTargeting)data[3]; - routine = (SpellRoutine)data[4]; - gfx = data[5]; - palette = data[6]; - } - - public override string ToString() - { - return Index.ToString() + ": " + Name; - } - } - - public partial class FF1Rom : NesRom - { - public const int MagicOffset = 0x301E0; - public const int MagicSize = 8; - public const int MagicCount = 64; - public const int MagicNamesOffset = 0x2BE03; - public const int MagicNameSize = 5; - public const int MagicTextPointersOffset = 0x304C0; - public const int MagicPermissionsOffset = 0x3AD18; - public const int MagicPermissionsSize = 8; - public const int MagicPermissionsCount = 12; - public const int MagicOutOfBattleOffset = 0x3AEFA; - public const int MagicOutOfBattleSize = 7; - public const int MagicOutOfBattleCount = 13; - - public const int OldLevelUpDataOffset = 0x2D094; // this was moved to bank 1B - public const int NewLevelUpDataOffset = 0x6CDA9; // this was moved from bank 1B - public const int oldKnightNinjaMaxMPOffset = 0x6C907; - public const int newKnightNinjaMaxMPOffset = 0x6D344; - - public const int ConfusedSpellIndexOffset = 0x3321E; - public const int FireSpellIndex = 4; - - public const int WeaponOffset = 0x30000; - public const int WeaponSize = 8; - public const int WeaponCount = 40; - - public const int ArmorOffset = 0x30140; - public const int ArmorSize = 4; - public const int ArmorCount = 40; - - public void ShuffleMagicLevels(EnemyScripts enemyScripts, MT19337 rng, bool enable, bool keepPermissions, bool tieredShuffle, bool mixSpellbooks) - { - if (!enable) - { - return; - } - - var magicSpells = GetSpells(); - - // First we have to un-interleave white and black spells. - var whiteSpells = magicSpells.Where((spell, i) => (i / 4) % 2 == 0).ToList(); - var blackSpells = magicSpells.Where((spell, i) => (i / 4) % 2 == 1).ToList(); - - if(tieredShuffle) - { - // weigh spell probability of landing in a tier based on where it was in the original game - var whiteSpellList = new List[3]; - var blackSpellList = new List[3]; - var whiteSpellFinalList = new List[3]; - var blackSpellFinalList = new List[3]; - int mergedSpellDoubler = 1; - whiteSpellList[0] = magicSpells.Where((spell, i) => (i / 4) % 2 == 0 && i < 24).ToList(); - whiteSpellList[1] = magicSpells.Where((spell, i) => (i / 4) % 2 == 0 && i < 48 && i >= 24).ToList(); - whiteSpellList[2] = magicSpells.Where((spell, i) => (i / 4) % 2 == 0 && i >= 48).ToList(); - blackSpellList[0] = magicSpells.Where((spell, i) => (i / 4) % 2 == 1 && i < 24).ToList(); - blackSpellList[1] = magicSpells.Where((spell, i) => (i / 4) % 2 == 1 && i < 48 && i >= 24).ToList(); - blackSpellList[2] = magicSpells.Where((spell, i) => (i / 4) % 2 == 1 && i >= 48).ToList(); - if(mixSpellbooks) - { - whiteSpellList[0] = whiteSpellList[0].Concat(blackSpellList[0]).ToList(); - whiteSpellList[1] = whiteSpellList[1].Concat(blackSpellList[1]).ToList(); - whiteSpellList[2] = whiteSpellList[2].Concat(blackSpellList[2]).ToList(); - mergedSpellDoubler = 2; - } - whiteSpellFinalList[0] = new List { }; - whiteSpellFinalList[1] = new List { }; - whiteSpellFinalList[2] = new List { }; - blackSpellFinalList[0] = new List { }; - blackSpellFinalList[1] = new List { }; - blackSpellFinalList[2] = new List { }; - whiteSpells.Clear(); - blackSpells.Clear(); - foreach (MagicSpell spell in whiteSpellList[2]) - { - // 70% chance of tier 7-8, 25% chance of tier 4-6, 5% chance of tier 1-3 - int diceRoll = rng.Between(0, 19); - if(diceRoll < 14) - { - whiteSpellFinalList[2].Add(spell); - } - else if (diceRoll < 19) - { - whiteSpellFinalList[1].Add(spell); - } - else - { - whiteSpellFinalList[0].Add(spell); - } - } - foreach (MagicSpell spell in whiteSpellList[1]) - { - // 60% chance of tier 4-6, 25% chance of tier 1-3, 15% chance of tier 7-8 - // if a section of the final list is full, move to another section - int diceRoll = rng.Between(0, 19); - if(diceRoll < 12) - { - if(whiteSpellFinalList[1].Count >= 12 * mergedSpellDoubler) - { - if(whiteSpellFinalList[0].Count >= 12 * mergedSpellDoubler) - { - whiteSpellFinalList[2].Add(spell); - } - else - { - whiteSpellFinalList[0].Add(spell); - } - } - else - { - whiteSpellFinalList[1].Add(spell); - } - } - else if (diceRoll < 17) - { - if(whiteSpellFinalList[0].Count >= 12 * mergedSpellDoubler) - { - if(whiteSpellFinalList[1].Count >= 12 * mergedSpellDoubler) - { - whiteSpellFinalList[2].Add(spell); - } - else - { - whiteSpellFinalList[1].Add(spell); - } - } - else - { - whiteSpellFinalList[0].Add(spell); - } - } - else - { - if(whiteSpellFinalList[2].Count >= 8 * mergedSpellDoubler) - { - if(whiteSpellFinalList[1].Count >= 12 * mergedSpellDoubler) - { - whiteSpellFinalList[0].Add(spell); - } - else - { - whiteSpellFinalList[1].Add(spell); - } - } - else - { - whiteSpellFinalList[2].Add(spell); - } - } - } - foreach(MagicSpell spell in whiteSpellList[0]) - { - // fill the remaining tiers with the tier 1-3 base magic - if(whiteSpellFinalList[0].Count >= 12 * mergedSpellDoubler) - { - if(whiteSpellFinalList[1].Count >= 12 * mergedSpellDoubler) - { - whiteSpellFinalList[2].Add(spell); - } - else - { - whiteSpellFinalList[1].Add(spell); - } - } - else - { - whiteSpellFinalList[0].Add(spell); - } - } - // and repeat the process for black magic if we didn't mix spellbooks - if(mixSpellbooks) - { - // if we mixed spellbooks, split the white (merged) spellbook in halves to set the black spell list - blackSpellFinalList[0] = whiteSpellFinalList[0].Take(12).ToList(); - whiteSpellFinalList[0] = whiteSpellFinalList[0].Except(blackSpellFinalList[0]).ToList(); - blackSpellFinalList[1] = whiteSpellFinalList[1].Take(12).ToList(); - whiteSpellFinalList[1] = whiteSpellFinalList[1].Except(blackSpellFinalList[1]).ToList(); - blackSpellFinalList[2] = whiteSpellFinalList[2].Take(8).ToList(); - whiteSpellFinalList[2] = whiteSpellFinalList[2].Except(blackSpellFinalList[2]).ToList(); - } - else - { - foreach (MagicSpell spell in blackSpellList[2]) - { - // 70% chance of tier 7-8, 25% chance of tier 4-6, 5% chance of tier 1-3 - int diceRoll = rng.Between(0, 19); - if (diceRoll < 14) - { - blackSpellFinalList[2].Add(spell); - } - else if (diceRoll < 19) - { - blackSpellFinalList[1].Add(spell); - } - else - { - blackSpellFinalList[0].Add(spell); - } - } - foreach (MagicSpell spell in blackSpellList[1]) - { - // 60% chance of tier 4-6, 25% chance of tier 1-3, 15% chance of tier 7-8 - // if a section of the final list is full, move to another section - int diceRoll = rng.Between(0, 19); - if (diceRoll < 12) - { - if (blackSpellFinalList[1].Count >= 12) - { - if (blackSpellFinalList[0].Count >= 12) - { - blackSpellFinalList[2].Add(spell); - } - else - { - blackSpellFinalList[0].Add(spell); - } - } - else - { - blackSpellFinalList[1].Add(spell); - } - } - else if (diceRoll < 17) - { - if (blackSpellFinalList[0].Count >= 12) - { - if (blackSpellFinalList[1].Count >= 12) - { - blackSpellFinalList[2].Add(spell); - } - else - { - blackSpellFinalList[1].Add(spell); - } - } - else - { - blackSpellFinalList[0].Add(spell); - } - } - else - { - if (blackSpellFinalList[2].Count >= 8) - { - if (blackSpellFinalList[1].Count >= 12) - { - blackSpellFinalList[0].Add(spell); - } - else - { - blackSpellFinalList[1].Add(spell); - } - } - else - { - blackSpellFinalList[2].Add(spell); - } - } - } - foreach (MagicSpell spell in blackSpellList[0]) - { - // fill the remaining tiers with the tier 1-3 base magic - if (blackSpellFinalList[0].Count >= 12) - { - if (blackSpellFinalList[1].Count >= 12) - { - blackSpellFinalList[2].Add(spell); - } - else - { - blackSpellFinalList[1].Add(spell); - } - } - else - { - blackSpellFinalList[0].Add(spell); - } - } - } - // shuffle each of the final lists - foreach(List spellList in whiteSpellFinalList) - { - spellList.Shuffle(rng); - } - if(!mixSpellbooks) - { - foreach (List spellList in blackSpellFinalList) - { - spellList.Shuffle(rng); - } - } - // and append each in turn to the whitespells / blackspells list - whiteSpells = whiteSpells.Concat(whiteSpellFinalList[0]).ToList(); - whiteSpells = whiteSpells.Concat(whiteSpellFinalList[1]).ToList(); - whiteSpells = whiteSpells.Concat(whiteSpellFinalList[2]).ToList(); - blackSpells = blackSpells.Concat(blackSpellFinalList[0]).ToList(); - blackSpells = blackSpells.Concat(blackSpellFinalList[1]).ToList(); - blackSpells = blackSpells.Concat(blackSpellFinalList[2]).ToList(); - } - else - { - if(mixSpellbooks) - { - var mergedList = magicSpells.ToList(); - mergedList.Shuffle(rng); - whiteSpells = mergedList.Where((spell, i) => (i / 4) % 2 == 0).ToList(); - blackSpells = mergedList.Where((spell, i) => (i / 4) % 2 == 1).ToList(); - } - else - { - whiteSpells.Shuffle(rng); - blackSpells.Shuffle(rng); - } - } - - // Now we re-interleave the spells. - var shuffledSpells = new List(); - for (int i = 0; i < MagicCount; i++) - { - var sourceIndex = 4 * (i / 8) + i % 4; - if ((i / 4) % 2 == 0) - { - shuffledSpells.Add(whiteSpells[sourceIndex]); - } - else - { - shuffledSpells.Add(blackSpells[sourceIndex]); - } - } - - Put(MagicOffset, shuffledSpells.Select(spell => spell.Data).Aggregate((seed, next) => seed + next)); - PutSpellNames(shuffledSpells); - Put(MagicTextPointersOffset, shuffledSpells.Select(spell => spell.TextPointer).ToArray()); - - if (keepPermissions) - { - // Shuffle the permissions the same way the spells were shuffled. - for (int c = 0; c < MagicPermissionsCount; c++) - { - SpellPermissions[(Classes)c] = SpellPermissions[(Classes)c].Select(x => (SpellSlots)shuffledSpells.FindIndex(y => y.Index == (int)x)).ToList(); - } - } - - // Map old indices to new indices. - var newIndices = new byte[MagicCount]; - for (byte i = 0; i < MagicCount; i++) - { - newIndices[shuffledSpells[i].Index] = i; - } - - // Fix enemy spell pointers to point to where the spells are now. - enemyScripts.UpdateSpellsIndices(newIndices.Select((s, i) => (i, s)).ToDictionary(s => (SpellByte)s.i, s => (SpellByte)s.s)); - - // Fix weapon and armor spell pointers to point to where the spells are now. - var weapons = Get(WeaponOffset, WeaponSize * WeaponCount).Chunk(WeaponSize); - foreach (var weapon in weapons) - { - if (weapon[3] != 0x00) - { - weapon[3] = (byte)(newIndices[weapon[3] - 1] + 1); - } - } - Put(WeaponOffset, weapons.SelectMany(weapon => weapon.ToBytes()).ToArray()); - - var armors = Get(ArmorOffset, ArmorSize * ArmorCount).Chunk(ArmorSize); - foreach (var armor in armors) - { - if (armor[3] != 0x00) - { - armor[3] = (byte)(newIndices[armor[3] - 1] + 1); - } - } - Put(ArmorOffset, armors.SelectMany(armor => armor.ToBytes()).ToArray()); - - // Fix the crazy out of battle spell system. - var outOfBattleSpellOffset = MagicOutOfBattleOffset; - for (int i = 0; i < MagicOutOfBattleCount; i++) - { - var oldSpellIndex = Data[outOfBattleSpellOffset] - 0xB0; - var newSpellIndex = newIndices[oldSpellIndex]; - - Put(outOfBattleSpellOffset, new[] { (byte)(newSpellIndex + 0xB0) }); - - outOfBattleSpellOffset += MagicOutOfBattleSize; - } - - // Confused enemies are supposed to cast FIRE, so figure out where FIRE ended up. - var newFireSpellIndex = shuffledSpells.FindIndex(spell => spell.Data == magicSpells[FireSpellIndex].Data); - Put(ConfusedSpellIndexOffset, new[] { (byte)newFireSpellIndex }); - } - - public List GetSpells() { - var spells = Get(MagicOffset, MagicSize * MagicCount).Chunk(MagicSize); - var pointers = Get(MagicTextPointersOffset, MagicCount); - - var battleMessages = new BattleMessages(this); - - var spellsList = spells.Select((spell, i) => new MagicSpell((byte)i, spell, ItemsText[176 + i], pointers[i], - pointers[i] > 0 ? battleMessages[pointers[i]-1] : "", - SpellPermissions.PermissionsFor((SpellSlots)i)) - ).ToList(); - - for (int i = 0; i < MagicOutOfBattleCount; i++) { - var spellIndex = Data[MagicOutOfBattleOffset + i*MagicOutOfBattleSize] - 0xB0; - spellsList[spellIndex].oobSpellRoutine = (OOBSpellRoutine)i; - } - return spellsList; - } - - public void PutSpells(List spellsList, EnemyScripts enemyScripts) { - - spellsList.Sort(delegate(MagicSpell a, MagicSpell b) { return a.Index.CompareTo(b.Index); }); - - var oldSpells = GetSpells(); - - foreach (var sp in spellsList) { - sp.writeData(this); - - if (sp.oobSpellRoutine == OOBSpellRoutine.None) { - continue; - } - - // update the out of battle magic code, it's a simple hardcoded table - // that compares the spell index and jumps to the desired routine - Data[MagicOutOfBattleOffset + (MagicOutOfBattleSize * (int)sp.oobSpellRoutine)] = (byte)(sp.Index + 0xB0); - - // update the effectivity of healing spells - int mask = 1; - while (sp.effect >= mask) { - mask = mask << 1; - } - mask = mask >> 1; - mask = mask - 1; - - - switch (sp.oobSpellRoutine) { - case OOBSpellRoutine.CURE: - // AND #mask - // ADC #effect - Put(0x3AF5E, new byte[] { 0x29, (byte)mask, 0x69, sp.effect }); // changing the oob code for CURE to reflect new values - break; - case OOBSpellRoutine.CUR2: - Put(0x3AF66, new byte[] { 0x29, (byte)mask, 0x69, sp.effect }); // changing the oob code for CUR2 to reflect new values - break; - case OOBSpellRoutine.CUR3: - Put(0x3AF6E, new byte[] { 0x29, (byte)mask, 0x69, sp.effect }); // changing the oob code for CUR3 to reflect new values - break; - - case OOBSpellRoutine.HEAL: - // AND #mask - // CLC - // ADC #effect - Put(0x3AFDB, new byte[] { 0x29, (byte)mask, 0x18, 0x69, sp.effect }); // changing the oob code for HEAL to reflect the above effect - break; - case OOBSpellRoutine.HEL2: - Put(0x3AFE4, new byte[] { 0x29, (byte)mask, 0x18, 0x69, sp.effect }); // changing the oob code for HEL2 to reflect the above effect - break; - case OOBSpellRoutine.HEL3: - Put(0x3AFED, new byte[] { 0x29, (byte)mask, 0x18, 0x69, sp.effect }); // changing the oob code for HEL3 to reflect the above effect - break; - default: - break; - } - } - - var sh = new SpellHelper(spellsList); - - Dictionary oldToNew = new(); - - for (int i = 0; i < oldSpells.Count; i++) { - var sp = oldSpells[i]; - IEnumerable<(Spell Id, MagicSpell Info)> result; - - result = sh.FindSpells(sp.routine, sp.targeting, sp.elem, sp.status, sp.oobSpellRoutine); - - if (!result.Any()) { - // Relax element - result = sh.FindSpells(sp.routine, sp.targeting, SpellElement.Any, sp.status, sp.oobSpellRoutine); - } - - if (!result.Any() && sp.routine != SpellRoutine.None) { - // Relax OOB spell routine - result = sh.FindSpells(sp.routine, sp.targeting, SpellElement.Any, sp.status, OOBSpellRoutine.None); - } - - if (!result.Any()) { - // Relax targeting - if (sp.targeting == SpellTargeting.AllEnemies) { - result = sh.FindSpells(sp.routine, SpellTargeting.OneEnemy, SpellElement.Any, sp.status, OOBSpellRoutine.None); - } else if (sp.targeting == SpellTargeting.Self) { - result = sh.FindSpells(sp.routine, SpellTargeting.OneCharacter, SpellElement.Any, sp.status, OOBSpellRoutine.None); - if (!result.Any()) { - result = sh.FindSpells(sp.routine, SpellTargeting.AllCharacters, SpellElement.Any, sp.status, OOBSpellRoutine.None); - } - } else if (sp.targeting == SpellTargeting.OneCharacter) { - result = sh.FindSpells(sp.routine, SpellTargeting.AllCharacters, SpellElement.Any, sp.status, OOBSpellRoutine.None); - if (!result.Any()) { - result = sh.FindSpells(sp.routine, SpellTargeting.Self, SpellElement.Any, sp.status, OOBSpellRoutine.None); - } - } else if (sp.targeting == SpellTargeting.AllCharacters) { - result = sh.FindSpells(sp.routine, SpellTargeting.OneCharacter, SpellElement.Any, sp.status, OOBSpellRoutine.None); - if (!result.Any()) { - result = sh.FindSpells(sp.routine, SpellTargeting.Self, SpellElement.Any, sp.status, OOBSpellRoutine.None); - } - } - } - - if (!result.Any()) { - throw new Exception($"Cannot find replacement spell for {sp.Name} with {sp.routine} {sp.status}"); - } - - if (result.Count() == 1) { - oldToNew[(Spell)((int)Spell.CURE + i)] = result.First().Item1; - continue; - } - - if (sp.routine == SpellRoutine.Damage || sp.routine == SpellRoutine.DamageUndead || - sp.routine == SpellRoutine.Heal || sp.routine == SpellRoutine.ArmorUp || - sp.routine == SpellRoutine.Sabr || sp.routine == SpellRoutine.Lock || - sp.routine == SpellRoutine.Ruse || sp.routine == SpellRoutine.Fear) - { - // Find the new spell that's closest to the old spell - // based on effectivity - int minimum = 256; - foreach (var candidate in result) { - int diff = Math.Abs(candidate.Item2.effect - sp.effect); - if (diff < minimum) { - minimum = diff; - oldToNew[(Spell)((int)Spell.CURE + i)] = candidate.Item1; - } - } - } else { - int minimum = 256; - // Find the new spell that's closest to the old spell - // based on accuracy - foreach (var candidate in result) { - var diff = Math.Abs(candidate.Item2.accuracy - sp.accuracy); - if (diff < minimum) { - minimum = diff; - oldToNew[(Spell)((int)Spell.CURE + i)] = candidate.Item1; - } - } - } - } - - /* - foreach (var kv in oldToNew) { - Console.WriteLine($"{(int)kv.Key - (int)Spell.CURE} -> {(int)kv.Value - (int)Spell.CURE}"); - Console.WriteLine($"{oldSpells[(int)kv.Key - (int)Spell.CURE]} -> {spellsList[(int)kv.Value - (int)Spell.CURE]}"); - } - */ - - // Fix enemy spell pointers to point to where the spells are now. - enemyScripts.UpdateSpellsIndices(oldToNew.ToDictionary(s => (SpellByte)(s.Key - (byte)Spell.CURE), s => (SpellByte)(s.Value - (byte)Spell.CURE))); - - // Fix weapon and armor spell pointers to point to where the spells are now. - foreach (var wep in Weapon.LoadAllWeapons(this, null)) { - if (wep.Spell != Spell.None) { - wep.SpellIndex = (byte)((int)oldToNew[wep.Spell] - (int)Spell.CURE + 1); - } - wep.writeWeaponMemory(this); - } - - foreach (var arm in Armor.LoadAllArmors(this, null)) { - if (arm.Spell != Spell.None) { - arm.SpellIndex = (byte)((int)oldToNew[arm.Spell] - (int)Spell.CURE + 1); - } - arm.writeArmorMemory(this); - } - - // Confused enemies are supposed to cast FIRE, so - // pick a single-target damage spell. - var confSpell = sh.FindSpells(SpellRoutine.Damage, SpellTargeting.OneEnemy); - if (!confSpell.Any()) { - throw new Exception("Missing a single-target damage spell to use for confused status"); - } - Put(ConfusedSpellIndexOffset, new[] { (byte)confSpell.First().Item2.Index }); - } - - public void PutSpellNames(List spells) - { - - for(int i = 0; i < spells.Count; i++) - { - ItemsText[176 + i] = spells[i].Name; - } - } - - public void SpellNames(Flags flags, Preferences preferences, MT19337 rng) - { - AccessibleSpellNames(preferences.AccessibleSpellNames && !(bool)flags.GenerateNewSpellbook); - MixUpSpellNames(flags.SpellNameMadness, rng); - } - - public void AccessibleSpellNames(bool enable) - { - // If Spellcrafter mode is on, abort. We need a check here as the setting on the site can be in a random state. - if (!enable) - { - return; - } - - var magicSpells = GetSpells(); - - // Since this can be performed independent of the magic shuffling, we can't assume the location of spell names. - // We will loop through the spell list and replace the appropriate names as we find them. - for (int i = 0; i < magicSpells.Count; i++) - { - MagicSpell newSpell = magicSpells[i]; - string spellName = magicSpells[i].Name; - - switch (spellName) - { - // Note that 3 letter spell names actually have a trailing space - case "LIT ": - newSpell.Name = "THUN"; - break; - case "LIT2": - newSpell.Name = "THN2"; - break; - case "LIT3": - newSpell.Name = "THN3"; - break; - case "FAST": - newSpell.Name = "HAST"; - break; - case "SLEP": - newSpell.Name = "DOZE"; - break; - case "SLP2": - newSpell.Name = "DOZ2"; - break; - - case "HARM": - newSpell.Name = "DIA "; - break; - case "HRM2": - newSpell.Name = "DIA2"; - break; - case "HRM3": - newSpell.Name = "DIA3"; - break; - case "HRM4": - newSpell.Name = "DIA4"; - break; - case "ALIT": - newSpell.Name = "ATHN"; - break; - case "AMUT": - newSpell.Name = "VOX "; - break; - case "FOG ": - newSpell.Name = "PROT"; - break; - case "FOG2": - newSpell.Name = "PRO2"; - break; - case "FADE": - newSpell.Name = "HOLY"; - break; - } - - // Update the entry in the list - magicSpells[i] = newSpell; - } - - // Now update the spell names! - PutSpellNames(magicSpells); - } - - public void MixUpSpellNames(SpellNameMadness mode, MT19337 rng) - { - if (mode == SpellNameMadness.MixedUp) - { - string[] spellnames = new string[64]; - Array.Copy(ItemsText.ToList().ToArray(), 176, spellnames, 0, 64); - - var spellnamelist = new List(spellnames); - spellnamelist.Shuffle(rng); - - for (int i = 0; i < spellnamelist.Count; i++) - { - ItemsText[176 + i] = spellnamelist[i]; - } - } - else if (mode == SpellNameMadness.Madness) - { - List alphabet = new List { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" }; - List numbers = new List { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }; - - for (int i = 176; i < 176 + 64; i++) - { - ItemsText[i] = alphabet.PickRandom(rng) + alphabet.PickRandom(rng) + numbers.PickRandom(rng) + alphabet.PickRandom(rng); - } - } - } - - } - - public enum SpellNameMadness - { - [Description("None")] - None, - - [Description("MixedUp")] - MixedUp, - - [Description("Madness")] - Madness - } - - public class SpellSlotInfo - { - public byte BattleId { get; set; } - public byte NameId { get; set; } - public int Level { get; set; } - public int Slot { get; set; } - public int MenuId { get; set; } - public SpellSchools SpellSchool { get; set; } - public byte PermissionByte { get; set; } - - public SpellSlotInfo(byte _battleId, int _level, int _slot, SpellSchools _spellSchool) - { - List permBytes = new() { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; - - BattleId = _battleId; - NameId = (byte)(_battleId + 0xB0); - Level = _level; - Slot = _slot; - MenuId = (_slot + 1) + (_spellSchool == SpellSchools.Black ? 4 : 0); - SpellSchool = _spellSchool; - //PermissionByte = permBytes[MenuId - 1]; - } - - public SpellSlotInfo() - { - List permBytes = new() { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; - - BattleId = 0x00; - NameId = 0x00; - Level = 0; - Slot = 0; - MenuId = 0; - SpellSchool = SpellSchools.White; - //PermissionByte = permBytes[MenuId - 1]; - } - } - - public static class SpellSlotStructure - { - public static SpellSlotInfo Cure = new SpellSlotInfo(0x00, 1, 0, SpellSchools.White); - public static SpellSlotInfo Harm = new SpellSlotInfo(0x01, 1, 1, SpellSchools.White); - public static SpellSlotInfo Fog = new SpellSlotInfo(0x02, 1, 2, SpellSchools.White); - public static SpellSlotInfo Ruse = new SpellSlotInfo(0x03, 1, 3, SpellSchools.White); - public static SpellSlotInfo Fire = new SpellSlotInfo(0x04, 1, 0, SpellSchools.Black); - public static SpellSlotInfo Slep = new SpellSlotInfo(0x05, 1, 1, SpellSchools.Black); - public static SpellSlotInfo Lock = new SpellSlotInfo(0x06, 1, 2, SpellSchools.Black); - public static SpellSlotInfo Lit = new SpellSlotInfo(0x07, 1, 3, SpellSchools.Black); - public static SpellSlotInfo Lamp = new SpellSlotInfo(0x08, 2, 0, SpellSchools.White); - public static SpellSlotInfo Mute = new SpellSlotInfo(0x09, 2, 1, SpellSchools.White); - public static SpellSlotInfo Alit = new SpellSlotInfo(0x0A, 2, 2, SpellSchools.White); - public static SpellSlotInfo Invs = new SpellSlotInfo(0x0B, 2, 3, SpellSchools.White); - public static SpellSlotInfo Ice = new SpellSlotInfo(0x0C, 2, 0, SpellSchools.Black); - public static SpellSlotInfo Dark = new SpellSlotInfo(0x0D, 2, 1, SpellSchools.Black); - public static SpellSlotInfo Tmpr = new SpellSlotInfo(0x0E, 2, 2, SpellSchools.Black); - public static SpellSlotInfo Slow = new SpellSlotInfo(0x0F, 2, 3, SpellSchools.Black); - public static SpellSlotInfo Cur2 = new SpellSlotInfo(0x10, 3, 0, SpellSchools.White); - public static SpellSlotInfo Hrm2 = new SpellSlotInfo(0x11, 3, 1, SpellSchools.White); - public static SpellSlotInfo Afir = new SpellSlotInfo(0x12, 3, 2, SpellSchools.White); - public static SpellSlotInfo Heal = new SpellSlotInfo(0x13, 3, 3, SpellSchools.White); - public static SpellSlotInfo Fir2 = new SpellSlotInfo(0x14, 3, 0, SpellSchools.Black); - public static SpellSlotInfo Hold = new SpellSlotInfo(0x15, 3, 1, SpellSchools.Black); - public static SpellSlotInfo Lit2 = new SpellSlotInfo(0x16, 3, 2, SpellSchools.Black); - public static SpellSlotInfo Lok2 = new SpellSlotInfo(0x17, 3, 3, SpellSchools.Black); - public static SpellSlotInfo Pure = new SpellSlotInfo(0x18, 4, 0, SpellSchools.White); - public static SpellSlotInfo Fear = new SpellSlotInfo(0x19, 4, 1, SpellSchools.White); - public static SpellSlotInfo Aice = new SpellSlotInfo(0x1A, 4, 2, SpellSchools.White); - public static SpellSlotInfo Amut = new SpellSlotInfo(0x1B, 4, 3, SpellSchools.White); - public static SpellSlotInfo Slp2 = new SpellSlotInfo(0x1C, 4, 0, SpellSchools.Black); - public static SpellSlotInfo Fast = new SpellSlotInfo(0x1D, 4, 1, SpellSchools.Black); - public static SpellSlotInfo Conf = new SpellSlotInfo(0x1E, 4, 2, SpellSchools.Black); - public static SpellSlotInfo Ice2 = new SpellSlotInfo(0x1F, 4, 3, SpellSchools.Black); - public static SpellSlotInfo Cur3 = new SpellSlotInfo(0x20, 5, 0, SpellSchools.White); - public static SpellSlotInfo Life = new SpellSlotInfo(0x21, 5, 1, SpellSchools.White); - public static SpellSlotInfo Hrm3 = new SpellSlotInfo(0x22, 5, 2, SpellSchools.White); - public static SpellSlotInfo Hel2 = new SpellSlotInfo(0x23, 5, 3, SpellSchools.White); - public static SpellSlotInfo Fir3 = new SpellSlotInfo(0x24, 5, 0, SpellSchools.Black); - public static SpellSlotInfo Bane = new SpellSlotInfo(0x25, 5, 1, SpellSchools.Black); - public static SpellSlotInfo Warp = new SpellSlotInfo(0x26, 5, 2, SpellSchools.Black); - public static SpellSlotInfo Slo2 = new SpellSlotInfo(0x27, 5, 3, SpellSchools.Black); - public static SpellSlotInfo Soft = new SpellSlotInfo(0x28, 6, 0, SpellSchools.White); - public static SpellSlotInfo Exit = new SpellSlotInfo(0x29, 6, 1, SpellSchools.White); - public static SpellSlotInfo Fog2 = new SpellSlotInfo(0x2A, 6, 2, SpellSchools.White); - public static SpellSlotInfo Inv2 = new SpellSlotInfo(0x2B, 6, 3, SpellSchools.White); - public static SpellSlotInfo Lit3 = new SpellSlotInfo(0x2C, 6, 0, SpellSchools.Black); - public static SpellSlotInfo Rub = new SpellSlotInfo(0x2D, 6, 1, SpellSchools.Black); - public static SpellSlotInfo Qake = new SpellSlotInfo(0x2E, 6, 2, SpellSchools.Black); - public static SpellSlotInfo Stun = new SpellSlotInfo(0x2F, 6, 3, SpellSchools.Black); - public static SpellSlotInfo Cur4 = new SpellSlotInfo(0x30, 7, 0, SpellSchools.White); - public static SpellSlotInfo Hrm4 = new SpellSlotInfo(0x31, 7, 1, SpellSchools.White); - public static SpellSlotInfo Arub = new SpellSlotInfo(0x32, 7, 2, SpellSchools.White); - public static SpellSlotInfo Hel3 = new SpellSlotInfo(0x33, 7, 3, SpellSchools.White); - public static SpellSlotInfo Ice3 = new SpellSlotInfo(0x34, 7, 0, SpellSchools.Black); - public static SpellSlotInfo Brak = new SpellSlotInfo(0x35, 7, 1, SpellSchools.Black); - public static SpellSlotInfo Sabr = new SpellSlotInfo(0x36, 7, 2, SpellSchools.Black); - public static SpellSlotInfo Blnd = new SpellSlotInfo(0x37, 7, 3, SpellSchools.Black); - public static SpellSlotInfo Lif2 = new SpellSlotInfo(0x38, 8, 0, SpellSchools.White); - public static SpellSlotInfo Fade = new SpellSlotInfo(0x39, 8, 1, SpellSchools.White); - public static SpellSlotInfo Wall = new SpellSlotInfo(0x3A, 8, 2, SpellSchools.White); - public static SpellSlotInfo Xfer = new SpellSlotInfo(0x3B, 8, 3, SpellSchools.White); - public static SpellSlotInfo Nuke = new SpellSlotInfo(0x3C, 8, 0, SpellSchools.Black); - public static SpellSlotInfo Stop = new SpellSlotInfo(0x3D, 8, 1, SpellSchools.Black); - public static SpellSlotInfo Zap = new SpellSlotInfo(0x3E, 8, 2, SpellSchools.Black); - public static SpellSlotInfo XXXX = new SpellSlotInfo(0x3F, 8, 3, SpellSchools.Black); - public static SpellSlotInfo None = new SpellSlotInfo(); - - public static List GetSpellSlots() - { - var fields = typeof(SpellSlotStructure).GetFields(BindingFlags.Public | BindingFlags.Static); - return fields.Where(f => f.FieldType == typeof(SpellSlotInfo)) - .Select(f => f.GetValue(null) as SpellSlotInfo) - .Where(t => t != null) - .ToList(); - } - } -} +using System.ComponentModel; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using FF1Lib.Helpers; + +namespace FF1Lib +{ + + public enum SpellRoutine : byte + { + None = 0, + Damage = 0x01, + DamageUndead = 0x02, + Heal = 0x07, + CureAilment = 0x08, + FullHeal = 0x0F, + ArmorUp = 0x09, + DefElement = 0x0A, + Fast = 0x0C, + Sabr = 0x0D, + Lock = 0x0E, + Ruse = 0x10, + PowerWord = 0x12, + InflictStatus = 0x03, + Life = 0xF0, + Smoke = 0xF1, + Slow = 0x04, + Fear = 0x05, + Xfer = 0x11, + } + + public enum SpellTargeting : byte + { + Any = 0xFF, + None = 0, + AllEnemies = 0x01, + OneEnemy = 0x02, + Self = 0x04, + AllCharacters = 0x08, + OneCharacter = 0x10 + } + + [Flags] + public enum SpellElement : byte + { + Any = 0b10101010, + None = 0x00, + Earth = 0b10000000, + Lightning = 0b01000000, + Ice = 0b00100000, + Fire = 0b00010000, + Death = 0b00001000, + Time = 0b00000100, + Poison = 0b00000010, + Status = 0b00000001, + All = 0xFF + } + + [Flags] + public enum SpellStatus : byte + { + None = 0, + Any = 0xFF, + Confuse = 0b10000000, + Mute = 0b01000000, + Dark = 0b00001000, + Stun = 0b00010000, + Sleep = 0b00100000, + Stone = 0b00000010, + Death = 0b00000001, + Poison = 0b00000100 + } + + public enum OOBSpellRoutine : byte { + CURE = 0, + CUR2 = 1, + CUR3 = 2, + CUR4 = 3, + HEAL = 4, + HEL3 = 5, + HEL2 = 6, + PURE = 7, + LIFE = 8, + LIF2 = 9, + WARP = 10, + SOFT = 11, + EXIT = 12, + None = 255 + } + + public enum MagicGraphic : byte { + None = 0, + BarOfLight = 176, + FourSparkles = 184, + Stars = 192, + EnergyBeam = 200, + EnergyFlare = 208, + GlowingBall = 216, + LargeSparkle = 224, + SparklingHand = 232 + } + + public enum SpellColor : byte { + White = 0x20, + Blue = 0x21, + Violet = 0x22, + Purple = 0x23, + Pink = 0x24, + PinkOrange = 0x25, + LightOrange = 0x26, + DarkOrange = 0x27, + Yellow = 0x28, + Green = 0x29, + LightGreen = 0x2A, + BlueGreen = 0x2B, + Teal = 0x2C, + Gray = 0x2D, + Black1 = 0x2E, + Black2 = 0x2F + } + + public enum SpellSchools + { + White = 0, + Black + } + + [JsonObject(MemberSerialization.OptIn)] + public class MagicSpell + { + [JsonProperty] + public byte Index; + + public bool ShouldSerializeIndex() { + return !isRegularSpell; + } + + public Blob Data; + + [JsonProperty] + public string Name; + + [JsonProperty] + public byte TextPointer; + + public bool ShouldSerializeTextPointer() { + return isRegularSpell; + } + + [JsonProperty] + public string Message; + + public bool ShouldSerializeMessage() { + return isRegularSpell; + } + + [JsonProperty] + public byte accuracy = 0; + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public SpellElement elem = 0; + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public SpellTargeting targeting = 0; + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public SpellRoutine routine = 0; + + [JsonProperty] + public byte effect = 0; + + public bool ShouldSerializeeffect() { + return routine != SpellRoutine.CureAilment && + routine != SpellRoutine.InflictStatus && + routine != SpellRoutine.DefElement && + routine != SpellRoutine.Slow && + routine != SpellRoutine.None && + routine != SpellRoutine.FullHeal && + routine != SpellRoutine.Xfer; + } + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public SpellStatus status { + get { return (SpellStatus)effect; } + set { effect = (byte)value; } + } + public bool ShouldSerializestatus() { + return routine == SpellRoutine.CureAilment || routine == SpellRoutine.InflictStatus; + } + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public SpellElement defelement { + get { + return (SpellElement)effect; + } + set { + effect = (byte)value; + } + } + public bool ShouldSerializedefelement() { + return routine == SpellRoutine.DefElement; + } + + public byte gfx = 0; + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public MagicGraphic MagicGraphic { + get { + return (MagicGraphic)gfx; + } + set { + gfx = (byte)value; + } + } + + public bool ShouldSerializegfx() { + return isRegularSpell; + } + + public byte palette = 0; + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public SpellColor SpellColor { + get { + return (SpellColor)palette; + } + set { + palette = (byte)value; + } + } + + public bool ShouldSerializeSpellColor() { + return isRegularSpell; + } + + void updateMagicIndex(byte level, byte slot, SpellSchools type) { + this.Index = (byte)((level-1) * 8 + (slot-1)); + if (type == SpellSchools.Black) { + this.Index += 4; + } + } + + [JsonProperty] + public byte Level { + get { + return (byte)((Index / 8)+1); + } + set { + if (value < 1 || value > 8) { + throw new Exception("Spell level must be between 1 and 8"); + } + this.updateMagicIndex(value, Slot, SpellSchool); + } + } + + public bool ShouldSerializeLevel() { + return isRegularSpell; + } + + [JsonProperty] + public byte Slot { + get { + return (byte)((Index % 4) + 1); + } + set { + if (value < 1 || value > 4) { + throw new Exception("Spell slot must be between 1 and 4"); + } + this.updateMagicIndex(Level, value, SpellSchool); + } + } + + public bool ShouldSerializeSlot() { + return isRegularSpell; + } + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public SpellSchools SpellSchool { + get { + if (Index % 8 < 4) { + return SpellSchools.White; + } + return SpellSchools.Black; + } + set { + this.updateMagicIndex(Level, Slot, value); + } + } + + public bool ShouldSerializeMagicType() { + return isRegularSpell; + } + + [JsonProperty] + [JsonConverter(typeof(StringEnumConverter))] + public OOBSpellRoutine oobSpellRoutine = OOBSpellRoutine.None; + + public bool ShouldSerializeoobSpellRoutine() { + return isRegularSpell; + } + + List _permissions = new(); + + [JsonProperty] + public string permissions { + get { + if (_permissions == null) { + return ""; + } + string ret = ""; + foreach (var c in _permissions) { + if (ret != "") { + ret += ", "; + } + ret += Enum.GetName(c); + } + return ret; + } + set { + _permissions.Clear(); + var sp = value.Split(","); + foreach (var cl in sp) { + _permissions.Add(Enum.Parse(cl)); + } + } + } + + public bool ShouldSerializepermissions() { + return isRegularSpell; + } + + bool isRegularSpell; + + public MagicSpell(byte _Index, + Blob _Data, + string _Name, + byte _TextPointer, + string _Message, + List __permissions) + { + Index = _Index; + Data = _Data; + Name = _Name; + TextPointer = _TextPointer; + Message = _Message; + _permissions = __permissions; + isRegularSpell = true; + this.decompressData(Data); + } + + public MagicSpell(byte _Index, + Blob _Data, + string _Name, + bool _isRegularSpell) + { + Index = _Index; + Data = _Data; + Name = _Name; + isRegularSpell = _isRegularSpell; + this.decompressData(Data); + } + + public MagicSpell() + { + Data = (Blob)new byte[8]; + isRegularSpell = true; + } + + public byte[] compressData() + { + Data[0] = accuracy; + Data[1] = effect; + Data[2] = (byte)elem; + Data[3] = (byte)targeting; + Data[4] = (byte)routine; + Data[5] = gfx; + Data[6] = palette; + Data[7] = 0x00; // last byte is always 00 + return Data; + } + + public void writeData(FF1Rom rom) { + compressData(); + if (isRegularSpell) { + rom.Put(FF1Rom.MagicOffset + FF1Rom.MagicSize * Index, Data); + rom.ItemsText[176 + Index] = Name; + } + } + + public void decompressData(byte[] data) + { + accuracy = data[0]; + effect = data[1]; + elem = (SpellElement)data[2]; + targeting = (SpellTargeting)data[3]; + routine = (SpellRoutine)data[4]; + gfx = data[5]; + palette = data[6]; + } + + public override string ToString() + { + return Index.ToString() + ": " + Name; + } + + public bool IsAoEAttack() + { + return ( + targeting == SpellTargeting.AllEnemies + && ( + routine == SpellRoutine.Damage + || ( + effect == (byte)SpellStatus.Death + && ( + routine == SpellRoutine.InflictStatus + || routine == SpellRoutine.PowerWord + ) + ) + ) + ); + } + } + + public partial class FF1Rom : NesRom + { + public const int MagicOffset = 0x301E0; + public const int MagicSize = 8; + public const int MagicCount = 64; + public const int MagicNamesOffset = 0x2BE03; + public const int MagicNameSize = 5; + public const int MagicTextPointersOffset = 0x304C0; + public const int MagicPermissionsOffset = 0x3AD18; + public const int MagicPermissionsSize = 8; + public const int MagicPermissionsCount = 12; + public const int MagicOutOfBattleOffset = 0x3AEFA; + public const int MagicOutOfBattleSize = 7; + public const int MagicOutOfBattleCount = 13; + + public const int OldLevelUpDataOffset = 0x2D094; // this was moved to bank 1B + public const int NewLevelUpDataOffset = 0x6CDA9; // this was moved from bank 1B + public const int oldKnightNinjaMaxMPOffset = 0x6C907; + public const int newKnightNinjaMaxMPOffset = 0x6D344; + + public const int ConfusedSpellIndexOffset = 0x3321E; + public const int FireSpellIndex = 4; + + public const int WeaponOffset = 0x30000; + public const int WeaponSize = 8; + public const int WeaponCount = 40; + + public const int ArmorOffset = 0x30140; + public const int ArmorSize = 4; + public const int ArmorCount = 40; + + public void ShuffleMagicLevels(EnemyScripts enemyScripts, MT19337 rng, bool enable, bool keepPermissions, bool tieredShuffle, bool mixSpellbooks) + { + if (!enable) + { + return; + } + + var magicSpells = GetSpells(); + + // First we have to un-interleave white and black spells. + var whiteSpells = magicSpells.Where((spell, i) => (i / 4) % 2 == 0).ToList(); + var blackSpells = magicSpells.Where((spell, i) => (i / 4) % 2 == 1).ToList(); + + if(tieredShuffle) + { + // weigh spell probability of landing in a tier based on where it was in the original game + var whiteSpellList = new List[3]; + var blackSpellList = new List[3]; + var whiteSpellFinalList = new List[3]; + var blackSpellFinalList = new List[3]; + int mergedSpellDoubler = 1; + whiteSpellList[0] = magicSpells.Where((spell, i) => (i / 4) % 2 == 0 && i < 24).ToList(); + whiteSpellList[1] = magicSpells.Where((spell, i) => (i / 4) % 2 == 0 && i < 48 && i >= 24).ToList(); + whiteSpellList[2] = magicSpells.Where((spell, i) => (i / 4) % 2 == 0 && i >= 48).ToList(); + blackSpellList[0] = magicSpells.Where((spell, i) => (i / 4) % 2 == 1 && i < 24).ToList(); + blackSpellList[1] = magicSpells.Where((spell, i) => (i / 4) % 2 == 1 && i < 48 && i >= 24).ToList(); + blackSpellList[2] = magicSpells.Where((spell, i) => (i / 4) % 2 == 1 && i >= 48).ToList(); + if(mixSpellbooks) + { + whiteSpellList[0] = whiteSpellList[0].Concat(blackSpellList[0]).ToList(); + whiteSpellList[1] = whiteSpellList[1].Concat(blackSpellList[1]).ToList(); + whiteSpellList[2] = whiteSpellList[2].Concat(blackSpellList[2]).ToList(); + mergedSpellDoubler = 2; + } + whiteSpellFinalList[0] = new List { }; + whiteSpellFinalList[1] = new List { }; + whiteSpellFinalList[2] = new List { }; + blackSpellFinalList[0] = new List { }; + blackSpellFinalList[1] = new List { }; + blackSpellFinalList[2] = new List { }; + whiteSpells.Clear(); + blackSpells.Clear(); + foreach (MagicSpell spell in whiteSpellList[2]) + { + // 70% chance of tier 7-8, 25% chance of tier 4-6, 5% chance of tier 1-3 + int diceRoll = rng.Between(0, 19); + if(diceRoll < 14) + { + whiteSpellFinalList[2].Add(spell); + } + else if (diceRoll < 19) + { + whiteSpellFinalList[1].Add(spell); + } + else + { + whiteSpellFinalList[0].Add(spell); + } + } + foreach (MagicSpell spell in whiteSpellList[1]) + { + // 60% chance of tier 4-6, 25% chance of tier 1-3, 15% chance of tier 7-8 + // if a section of the final list is full, move to another section + int diceRoll = rng.Between(0, 19); + if(diceRoll < 12) + { + if(whiteSpellFinalList[1].Count >= 12 * mergedSpellDoubler) + { + if(whiteSpellFinalList[0].Count >= 12 * mergedSpellDoubler) + { + whiteSpellFinalList[2].Add(spell); + } + else + { + whiteSpellFinalList[0].Add(spell); + } + } + else + { + whiteSpellFinalList[1].Add(spell); + } + } + else if (diceRoll < 17) + { + if(whiteSpellFinalList[0].Count >= 12 * mergedSpellDoubler) + { + if(whiteSpellFinalList[1].Count >= 12 * mergedSpellDoubler) + { + whiteSpellFinalList[2].Add(spell); + } + else + { + whiteSpellFinalList[1].Add(spell); + } + } + else + { + whiteSpellFinalList[0].Add(spell); + } + } + else + { + if(whiteSpellFinalList[2].Count >= 8 * mergedSpellDoubler) + { + if(whiteSpellFinalList[1].Count >= 12 * mergedSpellDoubler) + { + whiteSpellFinalList[0].Add(spell); + } + else + { + whiteSpellFinalList[1].Add(spell); + } + } + else + { + whiteSpellFinalList[2].Add(spell); + } + } + } + foreach(MagicSpell spell in whiteSpellList[0]) + { + // fill the remaining tiers with the tier 1-3 base magic + if(whiteSpellFinalList[0].Count >= 12 * mergedSpellDoubler) + { + if(whiteSpellFinalList[1].Count >= 12 * mergedSpellDoubler) + { + whiteSpellFinalList[2].Add(spell); + } + else + { + whiteSpellFinalList[1].Add(spell); + } + } + else + { + whiteSpellFinalList[0].Add(spell); + } + } + // and repeat the process for black magic if we didn't mix spellbooks + if(mixSpellbooks) + { + // if we mixed spellbooks, split the white (merged) spellbook in halves to set the black spell list + blackSpellFinalList[0] = whiteSpellFinalList[0].Take(12).ToList(); + whiteSpellFinalList[0] = whiteSpellFinalList[0].Except(blackSpellFinalList[0]).ToList(); + blackSpellFinalList[1] = whiteSpellFinalList[1].Take(12).ToList(); + whiteSpellFinalList[1] = whiteSpellFinalList[1].Except(blackSpellFinalList[1]).ToList(); + blackSpellFinalList[2] = whiteSpellFinalList[2].Take(8).ToList(); + whiteSpellFinalList[2] = whiteSpellFinalList[2].Except(blackSpellFinalList[2]).ToList(); + } + else + { + foreach (MagicSpell spell in blackSpellList[2]) + { + // 70% chance of tier 7-8, 25% chance of tier 4-6, 5% chance of tier 1-3 + int diceRoll = rng.Between(0, 19); + if (diceRoll < 14) + { + blackSpellFinalList[2].Add(spell); + } + else if (diceRoll < 19) + { + blackSpellFinalList[1].Add(spell); + } + else + { + blackSpellFinalList[0].Add(spell); + } + } + foreach (MagicSpell spell in blackSpellList[1]) + { + // 60% chance of tier 4-6, 25% chance of tier 1-3, 15% chance of tier 7-8 + // if a section of the final list is full, move to another section + int diceRoll = rng.Between(0, 19); + if (diceRoll < 12) + { + if (blackSpellFinalList[1].Count >= 12) + { + if (blackSpellFinalList[0].Count >= 12) + { + blackSpellFinalList[2].Add(spell); + } + else + { + blackSpellFinalList[0].Add(spell); + } + } + else + { + blackSpellFinalList[1].Add(spell); + } + } + else if (diceRoll < 17) + { + if (blackSpellFinalList[0].Count >= 12) + { + if (blackSpellFinalList[1].Count >= 12) + { + blackSpellFinalList[2].Add(spell); + } + else + { + blackSpellFinalList[1].Add(spell); + } + } + else + { + blackSpellFinalList[0].Add(spell); + } + } + else + { + if (blackSpellFinalList[2].Count >= 8) + { + if (blackSpellFinalList[1].Count >= 12) + { + blackSpellFinalList[0].Add(spell); + } + else + { + blackSpellFinalList[1].Add(spell); + } + } + else + { + blackSpellFinalList[2].Add(spell); + } + } + } + foreach (MagicSpell spell in blackSpellList[0]) + { + // fill the remaining tiers with the tier 1-3 base magic + if (blackSpellFinalList[0].Count >= 12) + { + if (blackSpellFinalList[1].Count >= 12) + { + blackSpellFinalList[2].Add(spell); + } + else + { + blackSpellFinalList[1].Add(spell); + } + } + else + { + blackSpellFinalList[0].Add(spell); + } + } + } + // shuffle each of the final lists + foreach(List spellList in whiteSpellFinalList) + { + spellList.Shuffle(rng); + } + if(!mixSpellbooks) + { + foreach (List spellList in blackSpellFinalList) + { + spellList.Shuffle(rng); + } + } + // and append each in turn to the whitespells / blackspells list + whiteSpells = whiteSpells.Concat(whiteSpellFinalList[0]).ToList(); + whiteSpells = whiteSpells.Concat(whiteSpellFinalList[1]).ToList(); + whiteSpells = whiteSpells.Concat(whiteSpellFinalList[2]).ToList(); + blackSpells = blackSpells.Concat(blackSpellFinalList[0]).ToList(); + blackSpells = blackSpells.Concat(blackSpellFinalList[1]).ToList(); + blackSpells = blackSpells.Concat(blackSpellFinalList[2]).ToList(); + } + else + { + if(mixSpellbooks) + { + var mergedList = magicSpells.ToList(); + mergedList.Shuffle(rng); + whiteSpells = mergedList.Where((spell, i) => (i / 4) % 2 == 0).ToList(); + blackSpells = mergedList.Where((spell, i) => (i / 4) % 2 == 1).ToList(); + } + else + { + whiteSpells.Shuffle(rng); + blackSpells.Shuffle(rng); + } + } + + // Now we re-interleave the spells. + var shuffledSpells = new List(); + for (int i = 0; i < MagicCount; i++) + { + var sourceIndex = 4 * (i / 8) + i % 4; + if ((i / 4) % 2 == 0) + { + shuffledSpells.Add(whiteSpells[sourceIndex]); + } + else + { + shuffledSpells.Add(blackSpells[sourceIndex]); + } + } + + Put(MagicOffset, shuffledSpells.Select(spell => spell.Data).Aggregate((seed, next) => seed + next)); + PutSpellNames(shuffledSpells); + Put(MagicTextPointersOffset, shuffledSpells.Select(spell => spell.TextPointer).ToArray()); + + if (keepPermissions) + { + // Shuffle the permissions the same way the spells were shuffled. + for (int c = 0; c < MagicPermissionsCount; c++) + { + SpellPermissions[(Classes)c] = SpellPermissions[(Classes)c].Select(x => (SpellSlots)shuffledSpells.FindIndex(y => y.Index == (int)x)).ToList(); + } + } + + // Map old indices to new indices. + var newIndices = new byte[MagicCount]; + for (byte i = 0; i < MagicCount; i++) + { + newIndices[shuffledSpells[i].Index] = i; + } + + // Fix enemy spell pointers to point to where the spells are now. + enemyScripts.UpdateSpellsIndices(newIndices.Select((s, i) => (i, s)).ToDictionary(s => (SpellByte)s.i, s => (SpellByte)s.s)); + + // Fix weapon and armor spell pointers to point to where the spells are now. + var weapons = Get(WeaponOffset, WeaponSize * WeaponCount).Chunk(WeaponSize); + foreach (var weapon in weapons) + { + if (weapon[3] != 0x00) + { + weapon[3] = (byte)(newIndices[weapon[3] - 1] + 1); + } + } + Put(WeaponOffset, weapons.SelectMany(weapon => weapon.ToBytes()).ToArray()); + + var armors = Get(ArmorOffset, ArmorSize * ArmorCount).Chunk(ArmorSize); + foreach (var armor in armors) + { + if (armor[3] != 0x00) + { + armor[3] = (byte)(newIndices[armor[3] - 1] + 1); + } + } + Put(ArmorOffset, armors.SelectMany(armor => armor.ToBytes()).ToArray()); + + // Fix the crazy out of battle spell system. + var outOfBattleSpellOffset = MagicOutOfBattleOffset; + for (int i = 0; i < MagicOutOfBattleCount; i++) + { + var oldSpellIndex = Data[outOfBattleSpellOffset] - 0xB0; + var newSpellIndex = newIndices[oldSpellIndex]; + + Put(outOfBattleSpellOffset, new[] { (byte)(newSpellIndex + 0xB0) }); + + outOfBattleSpellOffset += MagicOutOfBattleSize; + } + + // Confused enemies are supposed to cast FIRE, so figure out where FIRE ended up. + var newFireSpellIndex = shuffledSpells.FindIndex(spell => spell.Data == magicSpells[FireSpellIndex].Data); + Put(ConfusedSpellIndexOffset, new[] { (byte)newFireSpellIndex }); + } + + public List GetSpells() { + var spells = Get(MagicOffset, MagicSize * MagicCount).Chunk(MagicSize); + var pointers = Get(MagicTextPointersOffset, MagicCount); + + var battleMessages = new BattleMessages(this); + + var spellsList = spells.Select((spell, i) => new MagicSpell((byte)i, spell, ItemsText[176 + i], pointers[i], + pointers[i] > 0 ? battleMessages[pointers[i]-1] : "", + SpellPermissions.PermissionsFor((SpellSlots)i)) + ).ToList(); + + for (int i = 0; i < MagicOutOfBattleCount; i++) { + var spellIndex = Data[MagicOutOfBattleOffset + i*MagicOutOfBattleSize] - 0xB0; + spellsList[spellIndex].oobSpellRoutine = (OOBSpellRoutine)i; + } + return spellsList; + } + + public void PutSpells(List spellsList, EnemyScripts enemyScripts) { + + spellsList.Sort(delegate(MagicSpell a, MagicSpell b) { return a.Index.CompareTo(b.Index); }); + + var oldSpells = GetSpells(); + + foreach (var sp in spellsList) { + sp.writeData(this); + + if (sp.oobSpellRoutine == OOBSpellRoutine.None) { + continue; + } + + // update the out of battle magic code, it's a simple hardcoded table + // that compares the spell index and jumps to the desired routine + Data[MagicOutOfBattleOffset + (MagicOutOfBattleSize * (int)sp.oobSpellRoutine)] = (byte)(sp.Index + 0xB0); + + // update the effectivity of healing spells + int mask = 1; + while (sp.effect >= mask) { + mask = mask << 1; + } + mask = mask >> 1; + mask = mask - 1; + + + switch (sp.oobSpellRoutine) { + case OOBSpellRoutine.CURE: + // AND #mask + // ADC #effect + Put(0x3AF5E, new byte[] { 0x29, (byte)mask, 0x69, sp.effect }); // changing the oob code for CURE to reflect new values + break; + case OOBSpellRoutine.CUR2: + Put(0x3AF66, new byte[] { 0x29, (byte)mask, 0x69, sp.effect }); // changing the oob code for CUR2 to reflect new values + break; + case OOBSpellRoutine.CUR3: + Put(0x3AF6E, new byte[] { 0x29, (byte)mask, 0x69, sp.effect }); // changing the oob code for CUR3 to reflect new values + break; + + case OOBSpellRoutine.HEAL: + // AND #mask + // CLC + // ADC #effect + Put(0x3AFDB, new byte[] { 0x29, (byte)mask, 0x18, 0x69, sp.effect }); // changing the oob code for HEAL to reflect the above effect + break; + case OOBSpellRoutine.HEL2: + Put(0x3AFE4, new byte[] { 0x29, (byte)mask, 0x18, 0x69, sp.effect }); // changing the oob code for HEL2 to reflect the above effect + break; + case OOBSpellRoutine.HEL3: + Put(0x3AFED, new byte[] { 0x29, (byte)mask, 0x18, 0x69, sp.effect }); // changing the oob code for HEL3 to reflect the above effect + break; + default: + break; + } + } + + var sh = new SpellHelper(spellsList); + + Dictionary oldToNew = new(); + + for (int i = 0; i < oldSpells.Count; i++) { + var sp = oldSpells[i]; + IEnumerable<(Spell Id, MagicSpell Info)> result; + + result = sh.FindSpells(sp.routine, sp.targeting, sp.elem, sp.status, sp.oobSpellRoutine); + + if (!result.Any()) { + // Relax element + result = sh.FindSpells(sp.routine, sp.targeting, SpellElement.Any, sp.status, sp.oobSpellRoutine); + } + + if (!result.Any() && sp.routine != SpellRoutine.None) { + // Relax OOB spell routine + result = sh.FindSpells(sp.routine, sp.targeting, SpellElement.Any, sp.status, OOBSpellRoutine.None); + } + + if (!result.Any()) { + // Relax targeting + if (sp.targeting == SpellTargeting.AllEnemies) { + result = sh.FindSpells(sp.routine, SpellTargeting.OneEnemy, SpellElement.Any, sp.status, OOBSpellRoutine.None); + } else if (sp.targeting == SpellTargeting.Self) { + result = sh.FindSpells(sp.routine, SpellTargeting.OneCharacter, SpellElement.Any, sp.status, OOBSpellRoutine.None); + if (!result.Any()) { + result = sh.FindSpells(sp.routine, SpellTargeting.AllCharacters, SpellElement.Any, sp.status, OOBSpellRoutine.None); + } + } else if (sp.targeting == SpellTargeting.OneCharacter) { + result = sh.FindSpells(sp.routine, SpellTargeting.AllCharacters, SpellElement.Any, sp.status, OOBSpellRoutine.None); + if (!result.Any()) { + result = sh.FindSpells(sp.routine, SpellTargeting.Self, SpellElement.Any, sp.status, OOBSpellRoutine.None); + } + } else if (sp.targeting == SpellTargeting.AllCharacters) { + result = sh.FindSpells(sp.routine, SpellTargeting.OneCharacter, SpellElement.Any, sp.status, OOBSpellRoutine.None); + if (!result.Any()) { + result = sh.FindSpells(sp.routine, SpellTargeting.Self, SpellElement.Any, sp.status, OOBSpellRoutine.None); + } + } + } + + if (!result.Any()) { + throw new Exception($"Cannot find replacement spell for {sp.Name} with {sp.routine} {sp.status}"); + } + + if (result.Count() == 1) { + oldToNew[(Spell)((int)Spell.CURE + i)] = result.First().Item1; + continue; + } + + if (sp.routine == SpellRoutine.Damage || sp.routine == SpellRoutine.DamageUndead || + sp.routine == SpellRoutine.Heal || sp.routine == SpellRoutine.ArmorUp || + sp.routine == SpellRoutine.Sabr || sp.routine == SpellRoutine.Lock || + sp.routine == SpellRoutine.Ruse || sp.routine == SpellRoutine.Fear) + { + // Find the new spell that's closest to the old spell + // based on effectivity + int minimum = 256; + foreach (var candidate in result) { + int diff = Math.Abs(candidate.Item2.effect - sp.effect); + if (diff < minimum) { + minimum = diff; + oldToNew[(Spell)((int)Spell.CURE + i)] = candidate.Item1; + } + } + } else { + int minimum = 256; + // Find the new spell that's closest to the old spell + // based on accuracy + foreach (var candidate in result) { + var diff = Math.Abs(candidate.Item2.accuracy - sp.accuracy); + if (diff < minimum) { + minimum = diff; + oldToNew[(Spell)((int)Spell.CURE + i)] = candidate.Item1; + } + } + } + } + + /* + foreach (var kv in oldToNew) { + Console.WriteLine($"{(int)kv.Key - (int)Spell.CURE} -> {(int)kv.Value - (int)Spell.CURE}"); + Console.WriteLine($"{oldSpells[(int)kv.Key - (int)Spell.CURE]} -> {spellsList[(int)kv.Value - (int)Spell.CURE]}"); + } + */ + + // Fix enemy spell pointers to point to where the spells are now. + enemyScripts.UpdateSpellsIndices(oldToNew.ToDictionary(s => (SpellByte)(s.Key - (byte)Spell.CURE), s => (SpellByte)(s.Value - (byte)Spell.CURE))); + + // Fix weapon and armor spell pointers to point to where the spells are now. + foreach (var wep in Weapon.LoadAllWeapons(this, null)) { + if (wep.Spell != Spell.None) { + wep.SpellIndex = (byte)((int)oldToNew[wep.Spell] - (int)Spell.CURE + 1); + } + wep.writeWeaponMemory(this); + } + + foreach (var arm in Armor.LoadAllArmors(this, null)) { + if (arm.Spell != Spell.None) { + arm.SpellIndex = (byte)((int)oldToNew[arm.Spell] - (int)Spell.CURE + 1); + } + arm.writeArmorMemory(this); + } + + // Confused enemies are supposed to cast FIRE, so + // pick a single-target damage spell. + var confSpell = sh.FindSpells(SpellRoutine.Damage, SpellTargeting.OneEnemy); + if (!confSpell.Any()) { + throw new Exception("Missing a single-target damage spell to use for confused status"); + } + Put(ConfusedSpellIndexOffset, new[] { (byte)confSpell.First().Item2.Index }); + } + + public void PutSpellNames(List spells) + { + + for(int i = 0; i < spells.Count; i++) + { + ItemsText[176 + i] = spells[i].Name; + } + } + + public void SpellNames(Flags flags, Preferences preferences, MT19337 rng) + { + AccessibleSpellNames(preferences.AccessibleSpellNames && !(bool)flags.GenerateNewSpellbook); + MixUpSpellNames(flags.SpellNameMadness, rng); + } + + public void AccessibleSpellNames(bool enable) + { + // If Spellcrafter mode is on, abort. We need a check here as the setting on the site can be in a random state. + if (!enable) + { + return; + } + + var magicSpells = GetSpells(); + + // Since this can be performed independent of the magic shuffling, we can't assume the location of spell names. + // We will loop through the spell list and replace the appropriate names as we find them. + for (int i = 0; i < magicSpells.Count; i++) + { + MagicSpell newSpell = magicSpells[i]; + string spellName = magicSpells[i].Name; + + switch (spellName) + { + // Note that 3 letter spell names actually have a trailing space + case "LIT ": + newSpell.Name = "THUN"; + break; + case "LIT2": + newSpell.Name = "THN2"; + break; + case "LIT3": + newSpell.Name = "THN3"; + break; + case "FAST": + newSpell.Name = "HAST"; + break; + case "SLEP": + newSpell.Name = "DOZE"; + break; + case "SLP2": + newSpell.Name = "DOZ2"; + break; + + case "HARM": + newSpell.Name = "DIA "; + break; + case "HRM2": + newSpell.Name = "DIA2"; + break; + case "HRM3": + newSpell.Name = "DIA3"; + break; + case "HRM4": + newSpell.Name = "DIA4"; + break; + case "ALIT": + newSpell.Name = "ATHN"; + break; + case "AMUT": + newSpell.Name = "VOX "; + break; + case "FOG ": + newSpell.Name = "PROT"; + break; + case "FOG2": + newSpell.Name = "PRO2"; + break; + case "FADE": + newSpell.Name = "HOLY"; + break; + } + + // Update the entry in the list + magicSpells[i] = newSpell; + } + + // Now update the spell names! + PutSpellNames(magicSpells); + } + + public void MixUpSpellNames(SpellNameMadness mode, MT19337 rng) + { + if (mode == SpellNameMadness.MixedUp) + { + string[] spellnames = new string[64]; + Array.Copy(ItemsText.ToList().ToArray(), 176, spellnames, 0, 64); + + var spellnamelist = new List(spellnames); + spellnamelist.Shuffle(rng); + + for (int i = 0; i < spellnamelist.Count; i++) + { + ItemsText[176 + i] = spellnamelist[i]; + } + } + else if (mode == SpellNameMadness.Madness) + { + List alphabet = new List { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" }; + List numbers = new List { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }; + + for (int i = 176; i < 176 + 64; i++) + { + ItemsText[i] = alphabet.PickRandom(rng) + alphabet.PickRandom(rng) + numbers.PickRandom(rng) + alphabet.PickRandom(rng); + } + } + } + + } + + public enum SpellNameMadness + { + [Description("None")] + None, + + [Description("MixedUp")] + MixedUp, + + [Description("Madness")] + Madness + } + + public class SpellSlotInfo + { + public byte BattleId { get; set; } + public byte NameId { get; set; } + public int Level { get; set; } + public int Slot { get; set; } + public int MenuId { get; set; } + public SpellSchools SpellSchool { get; set; } + public byte PermissionByte { get; set; } + + public SpellSlotInfo(byte _battleId, int _level, int _slot, SpellSchools _spellSchool) + { + List permBytes = new() { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; + + BattleId = _battleId; + NameId = (byte)(_battleId + 0xB0); + Level = _level; + Slot = _slot; + MenuId = (_slot + 1) + (_spellSchool == SpellSchools.Black ? 4 : 0); + SpellSchool = _spellSchool; + //PermissionByte = permBytes[MenuId - 1]; + } + + public SpellSlotInfo() + { + List permBytes = new() { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; + + BattleId = 0x00; + NameId = 0x00; + Level = 0; + Slot = 0; + MenuId = 0; + SpellSchool = SpellSchools.White; + //PermissionByte = permBytes[MenuId - 1]; + } + } + + public static class SpellSlotStructure + { + public static SpellSlotInfo Cure = new SpellSlotInfo(0x00, 1, 0, SpellSchools.White); + public static SpellSlotInfo Harm = new SpellSlotInfo(0x01, 1, 1, SpellSchools.White); + public static SpellSlotInfo Fog = new SpellSlotInfo(0x02, 1, 2, SpellSchools.White); + public static SpellSlotInfo Ruse = new SpellSlotInfo(0x03, 1, 3, SpellSchools.White); + public static SpellSlotInfo Fire = new SpellSlotInfo(0x04, 1, 0, SpellSchools.Black); + public static SpellSlotInfo Slep = new SpellSlotInfo(0x05, 1, 1, SpellSchools.Black); + public static SpellSlotInfo Lock = new SpellSlotInfo(0x06, 1, 2, SpellSchools.Black); + public static SpellSlotInfo Lit = new SpellSlotInfo(0x07, 1, 3, SpellSchools.Black); + public static SpellSlotInfo Lamp = new SpellSlotInfo(0x08, 2, 0, SpellSchools.White); + public static SpellSlotInfo Mute = new SpellSlotInfo(0x09, 2, 1, SpellSchools.White); + public static SpellSlotInfo Alit = new SpellSlotInfo(0x0A, 2, 2, SpellSchools.White); + public static SpellSlotInfo Invs = new SpellSlotInfo(0x0B, 2, 3, SpellSchools.White); + public static SpellSlotInfo Ice = new SpellSlotInfo(0x0C, 2, 0, SpellSchools.Black); + public static SpellSlotInfo Dark = new SpellSlotInfo(0x0D, 2, 1, SpellSchools.Black); + public static SpellSlotInfo Tmpr = new SpellSlotInfo(0x0E, 2, 2, SpellSchools.Black); + public static SpellSlotInfo Slow = new SpellSlotInfo(0x0F, 2, 3, SpellSchools.Black); + public static SpellSlotInfo Cur2 = new SpellSlotInfo(0x10, 3, 0, SpellSchools.White); + public static SpellSlotInfo Hrm2 = new SpellSlotInfo(0x11, 3, 1, SpellSchools.White); + public static SpellSlotInfo Afir = new SpellSlotInfo(0x12, 3, 2, SpellSchools.White); + public static SpellSlotInfo Heal = new SpellSlotInfo(0x13, 3, 3, SpellSchools.White); + public static SpellSlotInfo Fir2 = new SpellSlotInfo(0x14, 3, 0, SpellSchools.Black); + public static SpellSlotInfo Hold = new SpellSlotInfo(0x15, 3, 1, SpellSchools.Black); + public static SpellSlotInfo Lit2 = new SpellSlotInfo(0x16, 3, 2, SpellSchools.Black); + public static SpellSlotInfo Lok2 = new SpellSlotInfo(0x17, 3, 3, SpellSchools.Black); + public static SpellSlotInfo Pure = new SpellSlotInfo(0x18, 4, 0, SpellSchools.White); + public static SpellSlotInfo Fear = new SpellSlotInfo(0x19, 4, 1, SpellSchools.White); + public static SpellSlotInfo Aice = new SpellSlotInfo(0x1A, 4, 2, SpellSchools.White); + public static SpellSlotInfo Amut = new SpellSlotInfo(0x1B, 4, 3, SpellSchools.White); + public static SpellSlotInfo Slp2 = new SpellSlotInfo(0x1C, 4, 0, SpellSchools.Black); + public static SpellSlotInfo Fast = new SpellSlotInfo(0x1D, 4, 1, SpellSchools.Black); + public static SpellSlotInfo Conf = new SpellSlotInfo(0x1E, 4, 2, SpellSchools.Black); + public static SpellSlotInfo Ice2 = new SpellSlotInfo(0x1F, 4, 3, SpellSchools.Black); + public static SpellSlotInfo Cur3 = new SpellSlotInfo(0x20, 5, 0, SpellSchools.White); + public static SpellSlotInfo Life = new SpellSlotInfo(0x21, 5, 1, SpellSchools.White); + public static SpellSlotInfo Hrm3 = new SpellSlotInfo(0x22, 5, 2, SpellSchools.White); + public static SpellSlotInfo Hel2 = new SpellSlotInfo(0x23, 5, 3, SpellSchools.White); + public static SpellSlotInfo Fir3 = new SpellSlotInfo(0x24, 5, 0, SpellSchools.Black); + public static SpellSlotInfo Bane = new SpellSlotInfo(0x25, 5, 1, SpellSchools.Black); + public static SpellSlotInfo Warp = new SpellSlotInfo(0x26, 5, 2, SpellSchools.Black); + public static SpellSlotInfo Slo2 = new SpellSlotInfo(0x27, 5, 3, SpellSchools.Black); + public static SpellSlotInfo Soft = new SpellSlotInfo(0x28, 6, 0, SpellSchools.White); + public static SpellSlotInfo Exit = new SpellSlotInfo(0x29, 6, 1, SpellSchools.White); + public static SpellSlotInfo Fog2 = new SpellSlotInfo(0x2A, 6, 2, SpellSchools.White); + public static SpellSlotInfo Inv2 = new SpellSlotInfo(0x2B, 6, 3, SpellSchools.White); + public static SpellSlotInfo Lit3 = new SpellSlotInfo(0x2C, 6, 0, SpellSchools.Black); + public static SpellSlotInfo Rub = new SpellSlotInfo(0x2D, 6, 1, SpellSchools.Black); + public static SpellSlotInfo Qake = new SpellSlotInfo(0x2E, 6, 2, SpellSchools.Black); + public static SpellSlotInfo Stun = new SpellSlotInfo(0x2F, 6, 3, SpellSchools.Black); + public static SpellSlotInfo Cur4 = new SpellSlotInfo(0x30, 7, 0, SpellSchools.White); + public static SpellSlotInfo Hrm4 = new SpellSlotInfo(0x31, 7, 1, SpellSchools.White); + public static SpellSlotInfo Arub = new SpellSlotInfo(0x32, 7, 2, SpellSchools.White); + public static SpellSlotInfo Hel3 = new SpellSlotInfo(0x33, 7, 3, SpellSchools.White); + public static SpellSlotInfo Ice3 = new SpellSlotInfo(0x34, 7, 0, SpellSchools.Black); + public static SpellSlotInfo Brak = new SpellSlotInfo(0x35, 7, 1, SpellSchools.Black); + public static SpellSlotInfo Sabr = new SpellSlotInfo(0x36, 7, 2, SpellSchools.Black); + public static SpellSlotInfo Blnd = new SpellSlotInfo(0x37, 7, 3, SpellSchools.Black); + public static SpellSlotInfo Lif2 = new SpellSlotInfo(0x38, 8, 0, SpellSchools.White); + public static SpellSlotInfo Fade = new SpellSlotInfo(0x39, 8, 1, SpellSchools.White); + public static SpellSlotInfo Wall = new SpellSlotInfo(0x3A, 8, 2, SpellSchools.White); + public static SpellSlotInfo Xfer = new SpellSlotInfo(0x3B, 8, 3, SpellSchools.White); + public static SpellSlotInfo Nuke = new SpellSlotInfo(0x3C, 8, 0, SpellSchools.Black); + public static SpellSlotInfo Stop = new SpellSlotInfo(0x3D, 8, 1, SpellSchools.Black); + public static SpellSlotInfo Zap = new SpellSlotInfo(0x3E, 8, 2, SpellSchools.Black); + public static SpellSlotInfo XXXX = new SpellSlotInfo(0x3F, 8, 3, SpellSchools.Black); + public static SpellSlotInfo None = new SpellSlotInfo(); + + public static List GetSpellSlots() + { + var fields = typeof(SpellSlotStructure).GetFields(BindingFlags.Public | BindingFlags.Static); + return fields.Where(f => f.FieldType == typeof(SpellSlotInfo)) + .Select(f => f.GetValue(null) as SpellSlotInfo) + .Where(t => t != null) + .ToList(); + } + } +} diff --git a/FF1Lib/StartingItems/StartingEquipment.cs b/FF1Lib/StartingItems/StartingEquipment.cs index 34b687cb8..8a9659649 100644 --- a/FF1Lib/StartingItems/StartingEquipment.cs +++ b/FF1Lib/StartingItems/StartingEquipment.cs @@ -181,11 +181,7 @@ private void GetRandomEndgameWeapon(List items) private void GetRandomAoe(List items) { - var damageAoes = spellHelper.FindSpells(SpellRoutine.Damage, SpellTargeting.AllEnemies).Select(s => s.Id); - var instaAoes = spellHelper.FindSpells(SpellRoutine.InflictStatus, SpellTargeting.AllEnemies, SpellElement.Any, SpellStatus.Death).Select(s => s.Id); - var powerWordAoes = spellHelper.FindSpells(SpellRoutine.PowerWord, SpellTargeting.AllEnemies, SpellElement.Any, SpellStatus.Death).Select(s => s.Id); - - var spells = new HashSet(damageAoes.Concat(instaAoes).Concat(powerWordAoes)); + var spells = new HashSet(spellHelper.GetAoEAttackSpells().Select(s => s.Id)); var weaps = weaponsList.Where(w => spells.Contains(w.Spell)).Select(w => w.Id); var arms = armorsList.Where(w => spells.Contains(w.Spell)).Select(w => w.Id);