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