From 1570a1058241f4134f2aadcd1f7c1fb09ad0c8a5 Mon Sep 17 00:00:00 2001 From: markyys Date: Wed, 1 Feb 2023 09:21:39 -0500 Subject: [PATCH 1/6] Fix mixed tabs and spaces for indentation in Magic.cs. No functional change. --- FF1Lib/Magic.cs | 2483 +++++++++++++++++++++++------------------------ 1 file changed, 1241 insertions(+), 1242 deletions(-) diff --git a/FF1Lib/Magic.cs b/FF1Lib/Magic.cs index c4491fed5..f53464383 100644 --- a/FF1Lib/Magic.cs +++ b/FF1Lib/Magic.cs @@ -1,1242 +1,1241 @@ -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 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(); + } + } +} From 394bf60ee0ff843ca17c7590b882148e674ae762 Mon Sep 17 00:00:00 2001 From: markyys Date: Wed, 1 Feb 2023 12:20:01 -0500 Subject: [PATCH 2/6] Make it clear that you will not be able to receive a caster item if the caster item magic pool is set to "None" --- FF1Blazorizer/Tabs/ItemEquipmentTab.razor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From cfe3554711235f6bef9dec89912497b967031014 Mon Sep 17 00:00:00 2001 From: markyys Date: Wed, 1 Feb 2023 12:21:27 -0500 Subject: [PATCH 3/6] SpellHelper: Add a method which encapsulates the "find all AoE attack spells" logic --- FF1Lib/Helpers/SpellHelper.cs | 9 +++++++++ 1 file changed, 9 insertions(+) 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); + } } } From 2494f54a308a14a3c20c1ecf7400fd42615eec24 Mon Sep 17 00:00:00 2001 From: markyys Date: Wed, 1 Feb 2023 12:22:33 -0500 Subject: [PATCH 4/6] MagicSpell: Add a method which makes it easy to tell if a given spell is an AoE attack spell. This might be overly specific, but I do not know if a more generic function would be useful. --- FF1Lib/Magic.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/FF1Lib/Magic.cs b/FF1Lib/Magic.cs index f53464383..de256516d 100644 --- a/FF1Lib/Magic.cs +++ b/FF1Lib/Magic.cs @@ -411,6 +411,23 @@ 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 From 9dd0b45b01409f230eedb550adabe440be9ac949 Mon Sep 17 00:00:00 2001 From: markyys Date: Wed, 1 Feb 2023 12:23:18 -0500 Subject: [PATCH 5/6] StartingEquipment: Call the "get a list of all AoE attack spells" logic from its new shared location --- FF1Lib/StartingItems/StartingEquipment.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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); From 231936a54f0285017dd4bf701d108e9b4cf1ca3b Mon Sep 17 00:00:00 2001 From: markyys Date: Wed, 1 Feb 2023 12:24:37 -0500 Subject: [PATCH 6/6] ItemMagic: If we are supposed to start with an AoE attack item and item magic is not entirely disabled then make sure at least one item in the game has an AoE attack spell --- FF1Lib/ItemMagic.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) 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);