diff --git a/FF1Lib/Credits.cs b/FF1Lib/Credits.cs index e4db73cb2..97018534d 100644 --- a/FF1Lib/Credits.cs +++ b/FF1Lib/Credits.cs @@ -237,7 +237,6 @@ public partial class FF1Rom : NesRom "", " artea", " drcatdoctor", - " xxxxxxxxxxxxxxx", " nic0lette", " splitpunched", " onefineday", diff --git a/FF1Lib/Enemizer.cs b/FF1Lib/Enemizer.cs index 82ee89386..e3a92b967 100644 --- a/FF1Lib/Enemizer.cs +++ b/FF1Lib/Enemizer.cs @@ -1,6 +1,7 @@ -using System.ComponentModel; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using System.ComponentModel; +using static FF1Lib.FF1Rom; namespace FF1Lib { @@ -307,7 +308,7 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from else tier = 0; // and the AI doesn't care about inflicting poison or confusion through spells } - if(routine == 0x04) // decrease speed (SLOW) + if (routine == 0x04) // decrease speed (SLOW) { if (elem == 0) // only non-elemental slow gets preferential treatment, and it doesn't care about targeting { @@ -319,7 +320,7 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from else tier = 2; } - if(routine == 0x07 || routine == 0x06) // HP Up (CURE, HEAL) + if (routine == 0x07 || routine == 0x06) // HP Up (CURE, HEAL) { if (targeting == 0x04) // single caster { @@ -361,7 +362,7 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from tier = 4; } } - if(routine == 0x08) // neutralize status + if (routine == 0x08) // neutralize status { if ((effect & 0b0000_0001) != 0) { @@ -378,7 +379,7 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from tier = 1; // removing confuse, mute, or stun on a single target is a tier 1 } } - if(routine == 0x09) // armor up + if (routine == 0x09) // armor up { if (targeting == 0x04) { @@ -420,9 +421,9 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from tier = 4; } } - if(routine == 0x0A) // resist element + if (routine == 0x0A) // resist element { - if(targeting == 0x04) // self caster: resist all elements is tier 3, all other elements are tier 1 + if (targeting == 0x04) // self caster: resist all elements is tier 3, all other elements are tier 1 { if (effect == 0xFF) tier = 3; @@ -460,14 +461,14 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from else tier = 0; // spells which assist the enemy are useless } - if(routine == 0x0C) // FAST + if (routine == 0x0C) // FAST { if (targeting == 0x04 || targeting == 0x10) tier = 3; // tier 3 fast is fair for a regular monster else if (targeting == 0x08) tier = 4; // multi-target fast is tier 4 though } - if(routine == 0x0D) // attack up + if (routine == 0x0D) // attack up { if (targeting == 0x04 || targeting == 0x10) { @@ -494,7 +495,7 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from tier = 4; } } - if(routine == 0x0E) // reduce evasion (LOCK) + if (routine == 0x0E) // reduce evasion (LOCK) { if (targeting == 0x04 || targeting == 0x08 || targeting == 0x10) { @@ -508,11 +509,11 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from tier = 3; // only allow tier 3 for extremely strong locks with no element } } - if(routine == 0x0F) // HP Max (CUR4) + if (routine == 0x0F) // HP Max (CUR4) { tier = 4; // CUR4 is always tier 4 from an enemy's perspective } - if(routine == 0x10) // increase evasion (RUSE, INVS) + if (routine == 0x10) // increase evasion (RUSE, INVS) { if (targeting == 0x04 || targeting == 0x10) { @@ -537,7 +538,7 @@ public void calc_Enemy_SpellTier() // calculates the usefulness of a spell (from tier = 4; } } - if(routine == 0x11) // remove resistance (XFER) + if (routine == 0x11) // remove resistance (XFER) { if (targeting == 0x04 || targeting == 0x08 || targeting == 0x10) { @@ -596,25 +597,28 @@ public void decompressData(byte[] data) public class EnemyInfo { - public int index; - public string name; + public int index; + public string name; public int exp; public int gp; public int hp; public int morale; - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte AIscript; - public EnemyScriptInfo spellSkillScript { - get { - if (AIscript == 0xff) { - return null; - } - return allAIScripts[AIscript]; + public EnemyScriptInfo spellSkillScript + { + get + { + if (AIscript == 0xff) + { + return null; + } + return allAIScripts[AIscript]; + } } - } public int agility; public int absorb; public int num_hits; @@ -622,94 +626,104 @@ public EnemyScriptInfo spellSkillScript { public int damage; public int critrate; - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte atk_elem; - [JsonConverter(typeof(StringEnumConverter))] - public SpellElement AttackElement { - get { - return (SpellElement)atk_elem; + [JsonConverter(typeof(StringEnumConverter))] + public SpellElement AttackElement + { + get + { + return (SpellElement)atk_elem; + } } - } - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte atk_ailment; - [JsonConverter(typeof(StringEnumConverter))] - public SpellStatus AttackAilment { - get { - return (SpellStatus)atk_ailment; + [JsonConverter(typeof(StringEnumConverter))] + public SpellStatus AttackAilment + { + get + { + return (SpellStatus)atk_ailment; + } } - } - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte monster_type; - [JsonConverter(typeof(StringEnumConverter))] - public MonsterType MonsterType { - get { - return (MonsterType)monster_type; + [JsonConverter(typeof(StringEnumConverter))] + public MonsterType MonsterType + { + get + { + return (MonsterType)monster_type; + } } - } public int mdef; - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte elem_weakness; - [JsonConverter(typeof(StringEnumConverter))] - public SpellElement ElementalWeakness { - get { - return (SpellElement)elem_weakness; - } + [JsonConverter(typeof(StringEnumConverter))] + public SpellElement ElementalWeakness + { + get + { + return (SpellElement)elem_weakness; + } } - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte elem_resist; - [JsonConverter(typeof(StringEnumConverter))] - public SpellElement ElementalResist { - get { - return (SpellElement)elem_resist; + [JsonConverter(typeof(StringEnumConverter))] + public SpellElement ElementalResist + { + get + { + return (SpellElement)elem_resist; + } } - } - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public int tier; // enemy's tier rating, used by Enemizer to determine stats and Formation Generator to enforce certain placement rules - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public int skilltier = 0; - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte image; // the image used by this image, of the 52 unique monster images available to normal enemies (does not include fiends or chaos). - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte pal; // the palette normally used by this enemy. this and the enemy's image are not stored in game data directly, rather they are implied by data in the formations - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte tileset { get => (byte)((image >> 2) & 0b00001111); } - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public byte pic { get => (byte)(image & 0b00000011); } - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public bool Large { get => (image & 1) == 1; } - [JsonIgnoreAttribute] + [JsonIgnoreAttribute] public bool Small { get => (image & 1) == 0; } - [JsonIgnoreAttribute] - public List allAIScripts; + [JsonIgnoreAttribute] + public List allAIScripts; public byte[] compressData() // compresses the information of the enemy into an array of bytes to be placed in the game code { @@ -760,10 +774,11 @@ public void decompressData(byte[] data) elem_resist = data[19]; } - public void writeData(FF1Rom rom) { - var d = compressData(); - rom.Put(EnemyOffset + index * EnemySize, d); - } + public void writeData(FF1Rom rom) + { + var d = compressData(); + rom.Put(EnemyOffset + index * EnemySize, d); + } } public class FormationInfo @@ -898,7 +913,7 @@ public EnemizerTrackingInfo() enemiesInTileset[i] = new List { }; palettesInTileset[i] = new List { }; } - for(int i = 0; i < EnemyCount; ++i) + for (int i = 0; i < EnemyCount; ++i) { enemyZones[i] = new List { }; featured[i] = false; @@ -987,11 +1002,11 @@ public void ReadEnemyDataFromFormation(FormationInfo f, EnemyInfo[] enemystats) public void LogFeatured(FormationInfo f) { - for(int i = 0; i < 3; i++) + for (int i = 0; i < 3; i++) { if (f.monMin[i] > 0) featured[f.id[i]] = true; - if(i < 2) + if (i < 2) { if (f.monMin[i + 4] > 0) featured[f.id[i]] = true; @@ -1075,7 +1090,7 @@ private bool DoEnemizer_Formations(MT19337 rng, ZoneFormations zones, EnemyInfo[ en.enemyZones[mon].Add(i); } } - if(en.enemyZones[mon].Count == 0) + if (en.enemyZones[mon].Count == 0) { for (int i = 0; i < en.zone.Count; ++i) { @@ -1116,7 +1131,7 @@ private bool DoEnemizer_Formations(MT19337 rng, ZoneFormations zones, EnemyInfo[ if (i == en.imp_encounter || i == en.phantom_encounter || i == en.warmech_encounter) continue; // don't do these encounters (we will roll them as special exceptions) FormationInfo f = new FormationInfo(); // this is the formation we are hoping to write - // we will start narrowing down the list of monsters we can feature + // we will start narrowing down the list of monsters we can feature List availablemons = new List { }; // first, we check if all monsters have been featured yet if (uniqueEnemyIDs.Where(NotFeatured).Count() == 0) @@ -1128,7 +1143,8 @@ private bool DoEnemizer_Formations(MT19337 rng, ZoneFormations zones, EnemyInfo[ } } // first, we determine which zones we have yet to reach the minimum for. if they have yet to reach the minimum, they can join the union if the mon hasn't been featured yet - for (int j = 0; j < en.zone.Count; ++j) { + for (int j = 0; j < en.zone.Count; ++j) + { if (en.zone[j].forms.Count < en.zone[j].min) { availablemons = availablemons.Union(en.zone[j].zonemons.Where(NotFeatured)).ToList(); @@ -1158,7 +1174,7 @@ private bool DoEnemizer_Formations(MT19337 rng, ZoneFormations zones, EnemyInfo[ // now we pick one of the availablemons at random as our first mon f.Top = availablemons.PickRandom(rng); f.tileset = enemy[f.Top].tileset; // and we pick the corresponding tileset - // we need to select which zone we are aiming to fill on the B-Side (and if we require a Large-only or Small-only formation to do that, we set the shape of the formation accordingly) + // we need to select which zone we are aiming to fill on the B-Side (and if we require a Large-only or Small-only formation to do that, we set the shape of the formation accordingly) List availablezones = en.enemyZones[f.Top].Where(index => en.zone[index].forms.Count < en.zone[index].min).ToList(); if (availablezones.Count == 0) // if there are no zones available that haven't reached their mincount, check for maxcount instead availablezones = en.enemyZones[f.Top].Where(index => en.zone[index].forms.Count < en.zone[index].max).ToList(); @@ -1192,7 +1208,7 @@ private bool DoEnemizer_Formations(MT19337 rng, ZoneFormations zones, EnemyInfo[ } else if (f.shape == 0x01) { - availablemons = en.enemiesInTileset[f.tileset].Where(mon=> LargeAndNotFeatured(mon, f.Top)).ToList(); + availablemons = en.enemiesInTileset[f.tileset].Where(mon => LargeAndNotFeatured(mon, f.Top)).ToList(); if (availablemons.Count == 0) availablemons = en.enemiesInTileset[f.tileset].Where(mon => Large(mon) && mon != f.Top).ToList(); @@ -1207,7 +1223,7 @@ private bool DoEnemizer_Formations(MT19337 rng, ZoneFormations zones, EnemyInfo[ availablemons = en.enemiesInTileset[f.tileset].Where(mon => mon != f.Top).ToList(); // if all other mons in this tileset have been featured, all mons in this tileset are eligible as a second mon } f.id[1] = availablemons.PickRandom(rng); // and pick a random mon from the available IDs to be mon 2 (and the primary monster of the A-side) - // we will attempt to hit a zone with the A-Side but if we can't we'll just try to make an encounter as close as possible to the goal + // we will attempt to hit a zone with the A-Side but if we can't we'll just try to make an encounter as close as possible to the goal availablezones = en.enemyZones[f.id[1]].Where(index => en.zone[index].forms.Count < en.zone[index].min).ToList(); if (availablezones.Count == 0) // if there are no zones available that haven't reached their mincount, check for maxcount instead availablezones = en.enemyZones[f.id[1]].Where(index => en.zone[index].forms.Count < en.zone[index].max).ToList(); @@ -1403,13 +1419,13 @@ private bool DoEnemizer_Formations(MT19337 rng, ZoneFormations zones, EnemyInfo[ traptile_formations[(int)TrapTiles.TRAP_SLIMES] = en.zone[9].forms[1]; traptile_formations[(int)TrapTiles.TRAP_PHANTOM] = en.phantom_encounter; // proceed to place mons in all zones - foreach(Enemizer_Zone zone in en.zone) + foreach (Enemizer_Zone zone in en.zone) { zone.forms.Shuffle(rng); int loop = 0; int f = 0; int a = 0; - while(f < zone.forms.Count) + while (f < zone.forms.Count) { if (loop == 0) { @@ -1684,7 +1700,7 @@ private byte[] ENF_DrawBossEncounter(EnemizerTrackingInfo en, MT19337 rng, Enemy f.shape = shape; f.monMin[2] = count; f.monMax[2] = count; // always produce the necessary number of enemies - // draw mons for B-Side + // draw mons for B-Side List availablemons = en.enemiesInTileset[f.tileset].ToList(); switch (f.shape) { @@ -1785,7 +1801,7 @@ public byte[] ENF_DrawForcedSingleFight(EnemizerTrackingInfo en, MT19337 rng, En f.surprise = 4; f.unrunnable_a = rng.Between(0, EnemizerUnrunnabilityWeight) + zoneA >= EnemizerUnrunnabilityWeight + 3 ? true : false; f.unrunnable_b = true; // the trap side is always unrunnable - // put on the finishing touches and log and compress this formation + // put on the finishing touches and log and compress this formation ENF_AssignPicAndPaletteBytes(enemy, f); en.LogFeatured(f); return f.compressData(); @@ -1857,7 +1873,7 @@ public void ENE_rollEnemyStats(MT19337 rng, EnemyInfo enemy) enemy.num_hits = rng.Between(1, 1 + (enemy.tier > 5 ? 5 : enemy.tier)); // roll valid damage tiers based on num_hits int minDamageTier = 4 + enemy.tier / 3 - enemy.num_hits; - if(minDamageTier < 1) + if (minDamageTier < 1) minDamageTier = 1; int maxDamageTier = 8 - enemy.num_hits; if (enemy.Large) @@ -2121,7 +2137,7 @@ private void DoEnemizer_EnemyPatternTablesOnly(MT19337 rng, EnemyInfo[] enemy, E 32, 34, 33, 35, 36, 38, 37, 39, 40, 42, 41, 43, 44, 46, 45, 47, 48, 50, 49, 51 }; - + byte[][] newEnemySprites = new byte[52][]; for (int i = 0; i < 13; ++i) { @@ -2130,12 +2146,12 @@ private void DoEnemizer_EnemyPatternTablesOnly(MT19337 rng, EnemyInfo[] enemy, E // or in the class itself. // Lots of magic numbers to map the enemy image LUT to the order of the images as they appear in the ROM. // All of this appears again below in DoEnemizer_Enemies(). - + newEnemyImageLUT[i * 4] = smallImages[i * 2]; - newEnemySprites[i * 4] = EnemySprites.GetLUT(smallImages[i * 2]); + newEnemySprites[i * 4] = EnemySprites.GetLUT(smallImages[i * 2]); newEnemyImageLUT[i * 4 + 2] = smallImages[i * 2 + 1]; - newEnemySprites[i * 4 + 1] = EnemySprites.GetLUT(smallImages[i * 2 + 1]); + newEnemySprites[i * 4 + 1] = EnemySprites.GetLUT(smallImages[i * 2 + 1]); newEnemyImageLUT[i * 4 + 1] = largeImages[i * 2]; newEnemySprites[i * 4 + 2] = EnemySprites.GetLUT(largeImages[i * 2]); @@ -2212,29 +2228,33 @@ private void DoEnemizer_EnemyPatternTablesOnly(MT19337 rng, EnemyInfo[] enemy, E } } - private bool DoEnemizer_Enemies(MT19337 rng, EnemyInfo[] enemy, SpellInfo[] spell, EnemySkillInfo[] skill, EnemyScriptInfo[] script, string[] enemyNames, string[] skillNames, bool shuffledSkillsOn, EnemizerTrackingInfo en) + private bool DoEnemizer_Enemies(MT19337 rng, EnemyInfo[] enemy, SpellInfo[] spell, EnemySkillInfo[] skill, + EnemyScriptInfo[] script, string[] enemyNames, string[] skillNames, bool shuffledSkillsOn, EnemizerTrackingInfo en) { + // these are the default tilesets + pics of the enemies we wish to shuffle List enemyImageLUT = new List { 0b00000000, 0b00000010, 0b00000001, 0b00000011, 0b00000100, 0b00000110, 0b00000101, 0b00000111, 0b00001000, 0b00001010, 0b00001001, 0b00001011, 0b00001100, 0b00001110, 0b00001101, 0b00001111, 0b00010000, 0b00010010, 0b00010001, 0b00010011, 0b00010100, 0b00010110, 0b00010101, 0b00010111, 0b00011000, 0b00011010, 0b00011001, 0b00011011, 0b00011100, 0b00011110, 0b00011101, 0b00011111, 0b00100000, 0b00100010, 0b00100001, 0b00100011, 0b00100100, 0b00100110, 0b00100101, 0b00100111, 0b00101000, 0b00101010, 0b00101001, 0b00101011, 0b00101100, 0b00101110, 0b00101101, 0b00101111, - 0b00110000, 0b00110010, 0b00110001, 0b00110011 }; // these are the default tilesets + pics of the enemies we wish to shuffle - // first, we shuffle what images appear in which pattern tables. small enemies can replace small enemies, and large enemies can replace large - List smallImages = new List { 0b00000000, 0b00000010, 0b00000100, 0b00000110, 0b00001000, 0b00001010, 0b00001100, 0b00001110, + 0b00110000, 0b00110010, 0b00110001, 0b00110011 }; + + // first, we shuffle what images appear in which pattern tables. small enemies can replace small enemies, and large enemies can replace large + List smallImages = new List { 0b00000000, 0b00000010, 0b00000100, 0b00000110, 0b00001000, 0b00001010, 0b00001100, 0b00001110, 0b00010000, 0b00010010, 0b00010100, 0b00010110, 0b00011000, 0b00011010, 0b00011100, 0b00011110, 0b00100000, 0b00100010, 0b00100100, 0b00100110, 0b00101000, 0b00101010, 0b00101100, 0b00101110, 0b00110000, 0b00110010 }; - List largeImages = new List { 0b00000001, 0b00000011, 0b00000101, 0b00000111, 0b00001001, 0b00001011, 0b00001101, 0b00001111, + + List largeImages = new List { 0b00000001, 0b00000011, 0b00000101, 0b00000111, 0b00001001, 0b00001011, 0b00001101, 0b00001111, 0b00010001, 0b00010011, 0b00010101, 0b00010111, 0b00011001, 0b00011011, 0b00011101, 0b00011111, 0b00100001, 0b00100011, 0b00100101, 0b00100111, 0b00101001, 0b00101011, 0b00101101, 0b00101111, 0b00110001, 0b00110011 }; + smallImages.Shuffle(rng); largeImages.Shuffle(rng); - //byte[] newPatternTableData = new byte[0x6800]; - //patterntabledata.CopyTo(newPatternTableData, 0); - List newEnemyImageLUT = new List { 0, 2, 1, 3, 4, 6, 5, 7, + + List newEnemyImageLUT = new List { 0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11, 12, 14, 13, 15, 16, 18, 17, 19, 20, 22, 21, 23, 24, 26, 25, 27, 28, 30, 29, 31, @@ -2245,1216 +2265,147 @@ private bool DoEnemizer_Enemies(MT19337 rng, EnemyInfo[] enemy, SpellInfo[] spel byte[][] newEnemySprites = new byte[52][]; for (int i = 0; i < 13; ++i) { - newEnemyImageLUT[i * 4] = smallImages[i * 2]; - newEnemySprites[i * 4] = EnemySprites.GetLUT(smallImages[i * 2]); + newEnemySprites[i * 4] = EnemySprites.GetLUT(smallImages[i * 2]); newEnemyImageLUT[i * 4 + 2] = smallImages[i * 2 + 1]; - newEnemySprites[i * 4 + 1] = EnemySprites.GetLUT(smallImages[i * 2 + 1]); + newEnemySprites[i * 4 + 1] = EnemySprites.GetLUT(smallImages[i * 2 + 1]); newEnemyImageLUT[i * 4 + 1] = largeImages[i * 2]; newEnemySprites[i * 4 + 2] = EnemySprites.GetLUT(largeImages[i * 2]); newEnemyImageLUT[i * 4 + 3] = largeImages[i * 2 + 1]; newEnemySprites[i * 4 + 3] = EnemySprites.GetLUT(largeImages[i * 2 + 1]); - } EnemySprites.CopyFrom(newEnemySprites); + // we are reproducing the image array because we only want to shuffle the images, not the monsters themselves List enemyImages = new List { }; for (int i = 0; i < Enemy.Lich; ++i) - enemyImages.Add(enemy[i].image); // we are reproducing the image array because we only want to shuffle the images, not the monsters themselves - List[] monsterClassVariants = new List[52]; // monster class modifiers that have effects on stats - monsterClassVariants[0] = new List { "Fr", "R.", "Z.", "Wr", "Wz", "Sea" }; // imp variants with special perks - monsterClassVariants[1] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; // iguana variants with special perks - monsterClassVariants[2] = new List { "Fr", "R.", "Z.", "Wr", "Wz" }; // wolf variants with special perks - monsterClassVariants[3] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; // giant variants with special perks - monsterClassVariants[4] = new List { "Fr", "Z.", "Wr", "Wz" }; // sahag variants with special perks - monsterClassVariants[5] = new List { "Fr", "Z." }; // shark variants with special perks - monsterClassVariants[6] = new List { "Fr", "R.", "Z.", "Wz" }; // pirate variants with special perks - monsterClassVariants[7] = new List { "Fr", "Z." }; // oddeye variants with special perks - monsterClassVariants[8] = new List { "Wz" }; // bone variants with special perks - monsterClassVariants[9] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; // ogre variants with special perks - monsterClassVariants[10] = new List { "Fr", "R.", "Z.", "Wr", "Wz", "Sea" }; // creep variants with special perks - monsterClassVariants[11] = new List { "Fr", "R.", "Z.", "Wr" }; // hyena variants with special perks - monsterClassVariants[12] = new List { "Fr", "R.", "Z.", "Sea" }; // asp variants with special perks - monsterClassVariants[13] = new List { "Fr", "R.", "Z.", "Wr", "Wz", "Sea" }; // bull variants with special perks - monsterClassVariants[14] = new List { "Fr", "R.", "Z.", "Sea" }; // crab variants with special perks - monsterClassVariants[15] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; // troll variants with special perks - monsterClassVariants[16] = new List { "Wz" }; // ghost variants with special perks - monsterClassVariants[17] = new List { "Fr", "R.", "Z.", "Sea" }; // worm variants with special perks - monsterClassVariants[18] = new List { "Sea" }; // wight variants with special perks - monsterClassVariants[19] = new List { "Fr", "R.", "Z.", "Sea" }; // eye variants with special perks - monsterClassVariants[20] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; // medusa variants with special perks - monsterClassVariants[21] = new List { "Fr", "R.", "Z.", "Sea" }; // pede variants with special perks - monsterClassVariants[22] = new List { "Fr", "R.", "Z.", "Wz" }; // catman variants with special perks - monsterClassVariants[23] = new List { "Fr", "R.", "Z.", "Wr" }; // tiger variants with special perks - monsterClassVariants[24] = new List { "Wz" }; // vamp variants with special perks - monsterClassVariants[25] = new List { }; // large elem variants with special perks - monsterClassVariants[26] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; // goyle variants with special perks - monsterClassVariants[27] = new List { }; // drake 1 variants with special perks - monsterClassVariants[28] = new List { "Wz" }; // flan variants with special perks - monsterClassVariants[29] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; // manticore variants with special perks - monsterClassVariants[30] = new List { "Fr", "R.", "Z.", "Sea" }; // spider variants with special perks - monsterClassVariants[31] = new List { "Fr", "R.", "Z." }; // ankylo variants with special perks - monsterClassVariants[32] = new List { "Wz" }; // mummy variants with special perks - monsterClassVariants[33] = new List { "Fr", "R.", "Z.", "Wr", "Wz" }; // wyvern variants with special perks - monsterClassVariants[34] = new List { "Fr", "R.", "Z." }; // bird variants with special perks - monsterClassVariants[35] = new List { "Fr", "R.", "Z.", "Sea" }; // tyro variants with special perks - monsterClassVariants[36] = new List { "Fr", "Z." }; // caribe variants with special perks - monsterClassVariants[37] = new List { "Fr", "Z.", "Wz" }; // ocho variants with special perks - monsterClassVariants[38] = new List { "Fr", "Z.", "Wr" }; // gator variants with special perks - monsterClassVariants[39] = new List { "Fr", "R.", "Z.", "Sea" }; // hydra variants with special perks - monsterClassVariants[40] = new List { }; // bot variants with special perks - monsterClassVariants[41] = new List { "Fr", "Z.", "Sea" }; // naga variants with special perks - monsterClassVariants[42] = new List { }; // small elem variants with special perks - monsterClassVariants[43] = new List { "Z.", "Wr", "Wz" }; // chimera variants with special perks - monsterClassVariants[44] = new List { "Z.", "Wz" }; // piscodemon variants with special perks - monsterClassVariants[45] = new List { }; // dragon 2 variants with special perks - monsterClassVariants[46] = new List { "Fr", "R.", "Z.", "Wz" }; // knight variants with special perks - monsterClassVariants[47] = new List { }; // golem variants with special perks - monsterClassVariants[48] = new List { "Fr", "R.", "Z.", "Wz" }; // badman variants with special perks - monsterClassVariants[49] = new List { "Fr", "R.", "Z.", "Wr", "Sea" }; // pony variants with special perks - monsterClassVariants[50] = new List { "Fr", "R.", "Z.", "Sea" }; // elf variants with special perks - monsterClassVariants[51] = new List { }; // mech variants with special perks - List[] monsterNameVariants = new List[52]; // name variants for monsters to prevent duplicates - bool[] monsterBaseNameUsed = new bool[52]; // true if the base name has been used, false if not - for (int i = 0; i < 52; ++i) - { - monsterBaseNameUsed[i] = false; // if enemy is not part of a variant class, by default it uses the base name for the monster - monsterNameVariants[i] = new List { "A.", "B.", "C.", "D.", "E.", "F.", "G.", "H.", "I.", "K.", "L.", "M.", "N.", "P.", "S.", "T.", "V.", "W.", "X." }; - } - monsterBaseNameUsed[25] = true; // large elemental - monsterBaseNameUsed[27] = true; // dragon 1 - monsterBaseNameUsed[42] = true; // small elemental - monsterBaseNameUsed[45] = true; // dragon 2 - enemyImageLUT.Shuffle(rng); // shuffle the LUT - whatever image was there in vanilla will be replaced with what is at its position in the LUT - enemyImages.Shuffle(rng); // and shuffle the enemy images themselves - for (int i = 0; i < GenericTilesetsCount; ++i) // generate the palettes for each tileset { - en.palettesInTileset[i].Clear(); - while (en.palettesInTileset[i].Count < 4) - { - int newPal = rng.Between(0, 0x3F); - if (en.palettesInTileset[i].Contains((byte)newPal)) - continue; - en.palettesInTileset[i].Add((byte)newPal); - } + enemyImages.Add(enemy[i].image); } - for (int i = 0; i < GenericTilesetsCount; ++i)// clear the list of enemies in each tileset - en.enemiesInTileset[i].Clear(); - List elementalsSelected = new List { 0, 1, 2, 3, 4, 5, 6, 7, 8 }; // List of elementals already selected + + // monster class modifiers that have effects on stats + List[] monsterClassVariants = InitializeMonsterClassVariants(); + // name variants for monsters to prevent duplicates + List[] monsterNameVariants = InitializeMonsterNameVariants(); + // true if the base name has been used, false if not + bool[] monsterBaseNameUsed = InitializeMonsterBaseNameUsed(); + + // shuffle the LUT - whatever image was there in vanilla will be replaced with what is at its position in the LUT + enemyImageLUT.Shuffle(rng); + // and shuffle the enemy images themselves + enemyImages.Shuffle(rng); + + // generate the palettes for each tileset + InitializeTilesetPalettes(rng, en); + // clear the list of enemies in each tileset + ClearEnemiesInTilesets(en); + + // List of elementals already selected + List elementalsSelected = new List { 0, 1, 2, 3, 4, 5, 6, 7, 8 }; List dragonsSelected = new List { 0, 1, 2, 3, 4, 5, 6, 7, 8 }; - List[] enemyImagePalettes = new List[52]; // create lists of palettes used for each enemy image + // create lists of palettes used for each enemy image + List[] enemyImagePalettes = new List[52]; for (int i = 0; i < 52; ++i) + { enemyImagePalettes[i] = new List { }; - for (byte i = 0; i < Enemy.Lich; ++i) // now start generating the enemies themselves. we stop before Lich and make special exceptions for various monsters + } + + // now start generating the enemies themselves. we stop before Lich and make special exceptions for various monsters + for (byte i = 0; i < Enemy.Lich; ++i) { - List perks = new List { MonsterPerks.PERK_GAINSTAT10, MonsterPerks.PERK_LOSESTAT10 }; // list of perks this monster is eligible to receive - enemy[i].image = enemyImageLUT[enemyImages[i]]; // assign the monster's pic + // list of perks this monster is eligible to receive + List perks = new List { MonsterPerks.PERK_GAINSTAT10, MonsterPerks.PERK_LOSESTAT10 }; + + // assign the monster's pic + enemy[i].image = enemyImageLUT[enemyImages[i]]; + + // enemy was outside the boundaries for acceptable tilesets - abort enemizer if (enemy[i].tileset >= GenericTilesetsCount) - return false; // enemy was outside the boundaries for acceptable tilesets - abort enemizer - bool oldSize = enemy[i].Large; // remember if enemy was small (false) or large (true) + { + return false; + } + + // remember if enemy was small (false) or large (true) + bool oldSize = enemy[i].Large; List acceptablepalettes = en.palettesInTileset[enemy[i].tileset].Except(enemyImagePalettes[enemy[i].image]).ToList(); - if(acceptablepalettes.Count == 0) + if (acceptablepalettes.Count == 0) + { acceptablepalettes = en.palettesInTileset[enemy[i].tileset].ToList(); + } + enemy[i].pal = acceptablepalettes.PickRandom(rng); enemyImagePalettes[enemy[i].image].Add(enemy[i].pal); + // generate the stats for each monster if (enemy[i].tier == -1) { // generate special monsters - switch (i) - { - case Enemy.Imp: - enemyNames[i] = "CHAMP"; - enemy[i].elem_weakness = 0b11111111; - enemy[i].monster_type = 0b01111111; - break; - case Enemy.Pirate: - enemy[i].image = (byte)newEnemyImageLUT.IndexOf(6); // image forced to where the PIRATE image was moved - enemy[i].pal = 0x0B; - break; - case Enemy.Phantom: - break; - case Enemy.Garland: - enemy[i].image = (byte)newEnemyImageLUT.IndexOf(46); // image forced to where the GARLAND image was moved - enemy[i].pal = 0x13; - break; - case Enemy.Astos: - enemy[i].image = (byte)newEnemyImageLUT.IndexOf(50); // image forced to where the ASTOS image was moved - enemy[i].pal = 0x06; - break; - case Enemy.WarMech: - switch (newEnemyImageLUT[enemy[i].image]) - { - case 0: - enemyNames[i] = "WarIMP"; - break; - case 1: - enemyNames[i] = "WarAGAMA"; - break; - case 2: - enemyNames[i] = "WarWOLF"; - break; - case 3: - enemyNames[i] = "WarGIANT"; - break; - case 4: - enemyNames[i] = "WarSAHAG"; - break; - case 5: - enemyNames[i] = "WarSHARK"; - break; - case 6: - enemyNames[i] = "WarBRUTE"; - break; - case 7: - enemyNames[i] = "WarEYE"; - break; - case 8: - enemyNames[i] = "WarBONE"; - break; - case 9: - enemyNames[i] = "WarHYENA"; - break; - case 10: - enemyNames[i] = "WarCREEP"; - break; - case 11: - enemyNames[i] = "WarOGRE"; - break; - case 12: - enemyNames[i] = "WarSNAKE"; - break; - case 13: - enemyNames[i] = "WarBULL"; - break; - case 14: - enemyNames[i] = "WarLBSTR"; - break; - case 15: - enemyNames[i] = "WarTROLL"; - break; - case 16: - enemyNames[i] = "WarGHOST"; - break; - case 17: - enemyNames[i] = "WarWORM"; - break; - case 18: - enemyNames[i] = "WarGEIST"; - break; - case 19: - enemyNames[i] = "WarEYE"; - break; - case 20: - enemyNames[i] = "WarLAMIA"; - break; - case 21: - enemyNames[i] = "WarPEDE"; - break; - case 22: - enemyNames[i] = "WarCAT"; - break; - case 23: - enemyNames[i] = "WarTIGER"; - break; - case 24: - enemyNames[i] = "WarVAMP"; - break; - case 25: - enemyNames[i] = "WarDJINN"; - break; - case 26: - enemyNames[i] = "WarGOYLE"; - break; - case 27: - enemyNames[i] = "WarDRAKE"; - break; - case 28: - enemyNames[i] = "WarOOZE"; - break; - case 29: - enemyNames[i] = "WarSPHNX"; - break; - case 30: - enemyNames[i] = "WarBUG"; - break; - case 31: - enemyNames[i] = "WarTURTL"; - break; - case 32: - enemyNames[i] = "WarMUMMY"; - break; - case 33: - enemyNames[i] = "WarWYRM"; - break; - case 34: - enemyNames[i] = "WarBIRD"; - break; - case 35: - enemyNames[i] = "WarREX"; - break; - case 36: - enemyNames[i] = "WarFISH"; - break; - case 37: - enemyNames[i] = "WarOCHO"; - break; - case 38: - enemyNames[i] = "WarGATOR"; - break; - case 39: - enemyNames[i] = "WarHYDRA"; - break; - case 40: - enemyNames[i] = "WarMECH"; - break; - case 41: - enemyNames[i] = "WarNAGA"; - break; - case 42: - enemyNames[i] = "WarWATER"; - break; - case 43: - enemyNames[i] = "WarBEAST"; - break; - case 44: - enemyNames[i] = "WarPISCO"; - break; - case 45: - enemyNames[i] = "WarDRAKE"; - break; - case 46: - enemyNames[i] = "WarGRLND"; - break; - case 47: - enemyNames[i] = "WarGOLEM"; - break; - case 48: - enemyNames[i] = "WarMAN"; - break; - case 49: - enemyNames[i] = "WarPONY"; - break; - case 50: - enemyNames[i] = "WarELF"; - break; - case 51: - enemyNames[i] = "WarMECH"; - break; - } - break; - } - } - else - { - en.enemiesInTileset[enemy[i].tileset].Add(i); // add enemy to enemiesInTileset unless it is a boss + HandleSpecialMonster(i, enemy, enemyNames, newEnemyImageLUT); + } else { + // add enemy to enemiesInTileset unless it is a boss + en.enemiesInTileset[enemy[i].tileset].Add(i); + // adjust HP, EXP, and GP if enemy changes size - if (oldSize && enemy[i].Small) // large became small - { - enemy[i].hp *= 4; - enemy[i].hp /= 5; - enemy[i].exp *= 9; - enemy[i].exp /= 10; - enemy[i].gp *= 9; - enemy[i].gp /= 10; - } - else if (!oldSize && enemy[i].Large) // small became large - { - enemy[i].hp *= 5; - enemy[i].hp /= 4; - enemy[i].exp *= 10; - enemy[i].exp /= 9; - enemy[i].gp *= 10; - enemy[i].gp /= 9; - } - int elemental = 9; // track elemental affinity for certain classes of monsters (elementals and dragons) - // generate monster's base elemental weakness/resist, type, and base name based on the monster image type - enemy[i].critrate = 1; // default crit rate of 1 for most enemies + AdjustStatsForSizeChange(enemy[i], oldSize); + + // track elemental affinity for certain classes of monsters (elementals and dragons) + int elemental = 9; + // default crit rate of 1 for most enemies + enemy[i].critrate = 1; // generate most enemy BASE stats ENE_rollEnemyStats(rng, enemy[i]); - switch (newEnemyImageLUT[enemy[i].image]) - { - case 0: // Imp - enemyNames[i] = "IMP"; - enemy[i].monster_type = 0b00000100; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 1: // Iguana - enemyNames[i] = "SAUR"; - enemy[i].monster_type = 0b00000010; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 2: // Wolf - enemyNames[i] = "WOLF"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 3: // Giant - enemyNames[i] = "GIANT"; - enemy[i].monster_type = 0b00000100; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 4: // Sahag - enemyNames[i] = "SAHAG"; - enemy[i].monster_type = 0b00100000; - enemy[i].elem_resist = 0b10010000; - enemy[i].elem_weakness = 0b01000000; - break; - case 5: // Shark - enemyNames[i] = "SHARK"; - enemy[i].monster_type = 0b00100000; - enemy[i].elem_resist = 0b10010000; - enemy[i].elem_weakness = 0b01000000; - break; - case 6: // Pirate - enemyNames[i] = "BRUTE"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b10000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 7: // Oddeye - enemyNames[i] = "EYES"; - enemy[i].monster_type = 0b00100000; - enemy[i].elem_resist = 0b10010000; - enemy[i].elem_weakness = 0b01000000; - break; - case 8: // Skeleton - enemyNames[i] = "BONE"; - enemy[i].monster_type = 0b00001000; - enemy[i].elem_resist = 0b00101011; - enemy[i].elem_weakness = 0b00010000; - break; - case 9: // Hyena - enemyNames[i] = "DOG"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 10: // Creep - enemyNames[i] = "CRAWL"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00010000; - break; - case 11: // Ogre - enemyNames[i] = "OGRE"; - enemy[i].monster_type = 0b00000100; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 12: // Asp - enemyNames[i] = "ASP"; - enemy[i].critrate = rng.Between(1, 20); - enemy[i].monster_type = 0b00000010; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 13: // Bull - enemyNames[i] = "BULL"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 14: // Crab - enemyNames[i] = "CRAB"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 15: // Troll - enemyNames[i] = "TROLL"; - enemy[i].monster_type = 0b10000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 16: // Spectral Undead - enemyNames[i] = "GHOST"; - enemy[i].monster_type = 0b00001001; - enemy[i].elem_resist = 0b10101011; - enemy[i].elem_weakness = 0b00010000; - break; - case 17: // Worm - enemyNames[i] = "WORM"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b10000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 18: // Zombie Undead - enemyNames[i] = "WIGHT"; - enemy[i].monster_type = 0b00001000; - enemy[i].elem_resist = 0b00101011; - enemy[i].elem_weakness = 0b00010000; - break; - case 19: // Eyes that are totally not Beholders plz no sue - enemyNames[i] = "EYE"; - enemy[i].monster_type = 0b01000000; - enemy[i].elem_resist = 0b10000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 20: // Medusa - enemyNames[i] = "LAMIA"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 21: // Pede - enemyNames[i] = "PEDE"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 22: // Were - enemyNames[i] = "CAT"; - enemy[i].monster_type = 0b00010000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 23: // Tiger - enemyNames[i] = "TIGER"; - enemy[i].critrate = rng.Between(20, 80); - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 24: // Blah, it's a Vampire! - enemyNames[i] = "VAMP"; - enemy[i].monster_type = 0b10001001; - enemy[i].elem_resist = 0b10101011; - enemy[i].elem_weakness = 0b00010000; - break; - case 25: // Large Elemental - if (elementalsSelected.Count > 0) - elemental = elementalsSelected.PickRandom(rng); - ENE_rollLargeElemental(rng, ref enemy[i], elemental); - switch (elemental) - { - case 1: - enemyNames[i] = "EARTH"; - break; - case 2: - enemyNames[i] = "STORM"; - break; - case 3: - enemyNames[i] = "ICE"; - break; - case 4: - enemyNames[i] = "FIRE"; - break; - case 5: - enemyNames[i] = "DEATH"; - break; - case 6: - enemyNames[i] = "CRONO"; - break; - case 7: - enemyNames[i] = "VENOM"; - break; - case 8: - enemyNames[i] = "DJINN"; - break; - default: - enemyNames[i] = "FORCE"; - break; - } - if (elemental != 9) - elementalsSelected.Remove(elemental); - break; - case 26: // Gargoyle - enemyNames[i] = "GOYLE"; - enemy[i].monster_type = 0b00000001; - enemy[i].elem_resist = 0b10000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 27: // Dragon Type 1 - if (dragonsSelected.Count > 0) - elemental = dragonsSelected.PickRandom(rng); - ENE_rollDragon(rng, ref enemy[i], elemental); - switch (elemental) - { - case 1: - enemyNames[i] = "Earth D"; - break; - case 2: - enemyNames[i] = "Elec D"; - break; - case 3: - enemyNames[i] = "Ice D"; - break; - case 4: - enemyNames[i] = "Fire D"; - break; - case 5: - enemyNames[i] = "Death D"; - break; - case 6: - enemyNames[i] = "Time D"; - break; - case 7: - enemyNames[i] = "Gas D"; - break; - case 8: - enemyNames[i] = "Magic D"; - break; - default: - enemyNames[i] = "DRAKE"; - break; - } - if (elemental != 9) - dragonsSelected.Remove(elemental); - break; - case 28: // Slime - enemyNames[i] = "FLAN"; - enemy[i].monster_type = 0b00000001; - enemy[i].elem_weakness = (byte)(rng.Between(1, 6) << 4); // can be fire, ice, fire+ice, lit, fire+lit, or ice+lit weak - enemy[i].elem_resist = (byte)((0b11111111 ^ enemy[i].elem_weakness) & 0b11111011); // resist all other elements except time - if (rng.Between(0, 1) == 1) - enemy[i].absorb = 255; // 50% chance of max absorb for this enemy type - break; - case 29: // Manticore - enemyNames[i] = "MANT"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 30: // Spider - enemyNames[i] = "BUG"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 31: // Ankylo - enemyNames[i] = "ANK"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 32: // Mummy - enemyNames[i] = "MUMMY"; - enemy[i].monster_type = 0b00001000; - enemy[i].elem_resist = 0b00101011; - enemy[i].elem_weakness = 0b00010000; - break; - case 33: // Wyvern - enemyNames[i] = "WYRM"; - enemy[i].monster_type = 0b00000010; - enemy[i].elem_resist = 0b10000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 34: // Jerk Bird - enemyNames[i] = "BIRD"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b10000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 35: // Steak - enemyNames[i] = "TYRO"; - enemy[i].critrate = rng.Between(20, 80); - enemy[i].monster_type = 0b00000010; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 36: // Pirahna - enemyNames[i] = "FISH"; - enemy[i].monster_type = 0b00100000; - enemy[i].elem_resist = 0b10010000; - enemy[i].elem_weakness = 0b01000000; - break; - case 37: // Ocho - enemyNames[i] = "OCHO"; - enemy[i].monster_type = 0b00100000; - enemy[i].elem_resist = 0b10010000; - enemy[i].elem_weakness = 0b01000000; - break; - case 38: // Gator - enemyNames[i] = "GATOR"; - enemy[i].monster_type = 0b00100010; - enemy[i].elem_resist = 0b10010000; - enemy[i].elem_weakness = 0b01000000; - break; - case 39: // Hydra - enemyNames[i] = "HYDRA"; - enemy[i].monster_type = 0b00000010; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 40: // Robot - enemyNames[i] = "BOT"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00001011; - enemy[i].elem_weakness = 0b01000000; - break; - case 41: // Naga - enemyNames[i] = "NAGA"; - enemy[i].monster_type = 0b01000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 42: // Small Elemental - enemyNames[i] = "AIR"; - if (elementalsSelected.Count > 0) - elemental = elementalsSelected.PickRandom(rng); - ENE_rollSmallElemental(rng, ref enemy[i], elemental); - switch (elemental) - { - case 1: - enemyNames[i] = "SHARD"; - break; - case 2: - enemyNames[i] = "WIND"; - break; - case 3: - enemyNames[i] = "WATER"; - break; - case 4: - enemyNames[i] = "FLARE"; - break; - case 5: - enemyNames[i] = "DOOM"; - break; - case 6: - enemyNames[i] = "TIME"; - break; - case 7: - enemyNames[i] = "BANE"; - break; - case 8: - enemyNames[i] = "MAGIC"; - break; - default: - enemyNames[i] = "AIR"; - break; - } - if (elemental != 9) - elementalsSelected.Remove(elemental); - break; - case 43: // Chimera - enemyNames[i] = "BEAST"; - enemy[i].monster_type = 0b00000010; - enemy[i].elem_resist = 0b10010000; - enemy[i].elem_weakness = 0b00100000; - break; - case 44: // Piscodemon - enemyNames[i] = "SQUID"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00110011; - enemy[i].elem_weakness = 0b00000000; - break; - case 45: // Dragon Type 2 - if (dragonsSelected.Count > 0) - elemental = dragonsSelected.PickRandom(rng); - ENE_rollDragon(rng, ref enemy[i], elemental); - switch (elemental) - { - case 1: - enemyNames[i] = "Earth D"; - break; - case 2: - enemyNames[i] = "Elec D"; - break; - case 3: - enemyNames[i] = "Ice D"; - break; - case 4: - enemyNames[i] = "Fire D"; - break; - case 5: - enemyNames[i] = "Death D"; - break; - case 6: - enemyNames[i] = "Time D"; - break; - case 7: - enemyNames[i] = "Gas D"; - break; - case 8: - enemyNames[i] = "Magic D"; - break; - default: - enemyNames[i] = "DRAGON"; - break; - } - if (elemental != 9) - dragonsSelected.Remove(elemental); - break; - case 46: // Knight Type 1 - enemyNames[i] = "KNIGHT"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 47: // Golem - enemyNames[i] = "GOLEM"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b01111011; - if (enemy[i].absorb > (enemy[i].tier < 3 ? 30 : 60)) - { - switch (rng.Between(0, 2)) - { - case 0: - enemy[i].elem_resist &= 0b10111111; - break; - case 1: - enemy[i].elem_resist &= 0b11011111; - break; - case 2: - enemy[i].elem_resist &= 0b11101111; - break; - } - } - enemy[i].elem_weakness = 0b00000000; - break; - case 48: // Knight Type 2 - enemyNames[i] = "RANGER"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 49: // Pony - enemyNames[i] = "PONY"; - enemy[i].monster_type = 0b00000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 50: // Elf - enemyNames[i] = "ELF"; - enemy[i].monster_type = 0b01000000; - enemy[i].elem_resist = 0b00000000; - enemy[i].elem_weakness = 0b00000000; - break; - case 51: // War Machine - enemyNames[i] = "MECH"; - enemy[i].monster_type = 0b10000000; - enemy[i].elem_resist = 0b00111011; - enemy[i].elem_weakness = 0b01000000; - break; - } + + // generate monster's base elemental weakness/resist, type, and base name based on the monster image type + ConfigureEnemyByImage(rng, i, enemy, enemyNames, newEnemyImageLUT, ref elemental, elementalsSelected, dragonsSelected); + // generate perk eligibility - perks.Add(MonsterPerks.PERK_LOWRESIST); - perks.Add(MonsterPerks.PERK_HIGHRESIST); - perks.Add(MonsterPerks.PERK_LOWWEAKNESS); - perks.Add(MonsterPerks.PERK_HIGHWEAKNESS); - perks.Add(MonsterPerks.PERK_PLUSONEHIT); - perks.Add(MonsterPerks.PERK_POISONTOUCH); - perks.Add(MonsterPerks.PERK_STUNSLEEPTOUCH); - perks.Add(MonsterPerks.PERK_MUTETOUCH); + AddEligiblePerks(perks); // set attack element and ailments to default value enemy[i].atk_ailment = 0b00000000; enemy[i].atk_elem = 0b00000000; + // apply global perks - bool hasGlobalPerk = false; - bool didntChooseVariantClass = true; // false if this monster rolled a variant class and thus doesn't need a name modifier - // if enemy has a global perk, apply it now and skip all other perks - if (i == Enemy.Crawl) - { - // Global Perk - multi-hitter with weak attacks but stun touch guaranteed (overrides existing atk ailment) - enemy[i].atk_elem = 0b00000001; - enemy[i].atk_ailment = 0b00010000; - enemy[i].num_hits = 8; - enemy[i].damage = 1; - hasGlobalPerk = true; - } - else if (i == Enemy.Coctrice) - { - // Global Perk - stonetouch (overrides other atk_elem and ailment), -1 hit if num_hits > 1 - enemy[i].atk_elem = 0b00000010; - enemy[i].atk_ailment = 0b00000010; - if (enemy[i].num_hits > 1) - enemy[i].num_hits--; - hasGlobalPerk = true; - } - else if (i == Enemy.Sorcerer) - { - // Global Perk - deathtouch - enemy[i].atk_elem = 0b00000000; - enemy[i].atk_ailment = 0b00000001; - hasGlobalPerk = true; - } - else - { - // else, roll the chance for an enemy to get a class modifier. Each class of monster can have Frost, Red, Zombie, Were, and Wizard variants, though some monster classes can't use some of these + // if enemy has a global perk, apply it now and skip all other perks + bool hasGlobalPerk = ApplyGlobalPerk(i, enemy, rng); + // false if this monster rolled a variant class and thus doesn't need a name modifier + bool didntChooseVariantClass = true; + + if (!hasGlobalPerk) { + // else, roll the chance for an enemy to get a class modifier. Each class of monster can have Frost, Red, Zombie, Were, and Wizard variants, though some monster classes can't use some of these // this perk restricts the monster from rolling any other perks - if (rng.Between(0, 11) < monsterClassVariants[newEnemyImageLUT[enemy[i].image]].Count()) - { - didntChooseVariantClass = false; - string classModifier = monsterClassVariants[newEnemyImageLUT[enemy[i].image]].PickRandom(rng); - switch (classModifier) // apply the traits of this enemy class - { - case "Wz": - if (enemy[i].AIscript == 0xFF && !shuffledSkillsOn) - { - enemy[i].AIscript = ENE_PickForcedAIScript(enemy[i].tier, rng); - } - break; - case "Fr": - enemy[i].elem_resist |= 0b00100000; - enemy[i].elem_resist &= 0b11101111; - enemy[i].elem_weakness |= 0b00010000; - enemy[i].elem_weakness &= 0b11011111; - enemy[i].atk_elem = 0b00100000; - break; - case "R.": - enemy[i].elem_resist |= 0b00010000; - enemy[i].elem_resist &= 0b11011111; - enemy[i].elem_weakness |= 0b00100000; - enemy[i].elem_weakness &= 0b11101111; - enemy[i].atk_elem = 0b00010000; - break; - case "Z.": - enemy[i].elem_resist |= 0b00101011; - enemy[i].elem_resist &= 0b11101111; - enemy[i].elem_weakness |= 0b00010000; - enemy[i].elem_weakness &= 0b11010100; - enemy[i].monster_type |= 0b00001000; - if (enemy[i].atk_ailment == 0) - { - enemy[i].atk_ailment = 0b00010000; - enemy[i].atk_elem = 0b00000001; - } - break; - case "Sea": - enemy[i].elem_resist |= 0b10010000; - enemy[i].elem_resist &= 0b10111111; - enemy[i].elem_weakness |= 0b01000000; - enemy[i].elem_weakness &= 0b01101111; - enemy[i].monster_type |= 0b00100000; - break; - case "Wr": - enemy[i].atk_ailment = 0b00000100; - enemy[i].atk_elem = 0b00000010; - enemy[i].monster_type |= 0b10010000; - break; - } - enemyNames[i] = classModifier + enemyNames[i]; // change the enemy's name - monsterClassVariants[newEnemyImageLUT[enemy[i].image]].Remove(classModifier); // remove this variant from the list of variants available for this enemy image - } + didntChooseVariantClass = TryApplyClassVariant(rng, i, enemy, enemyNames, newEnemyImageLUT, monsterClassVariants, shuffledSkillsOn); } + if (didntChooseVariantClass) { - // else, roll minor perks. there is a 75% chance to gain a perk, then 62.5%, 50%, and so on. monsters cannot gain more than 4 perks - int num_perks = 0; + // else, roll minor perks. there is a 75% chance to gain a perk, then 62.5%, 50%, and so on. monsters cannot gain more than 4 perks if (!hasGlobalPerk) { - while (rng.Between(0, 7) > 1 + num_perks && num_perks < 4) - { - // select from the list of available perks - MonsterPerks this_perk = perks.PickRandom(rng); - bool didPerkRoll = true; - switch (this_perk) - { - case MonsterPerks.PERK_GAINSTAT10: // pick a random stat to increase by 10%, +3% XP - switch (rng.Between(0, 2)) - { - case 0: - enemy[i].hp *= 11; - enemy[i].hp /= 10; - break; - case 1: - enemy[i].damage *= 11; - enemy[i].damage /= 10; - break; - case 2: - enemy[i].absorb *= 11; - enemy[i].absorb /= 10; - break; - } - enemy[i].exp *= 103; - enemy[i].exp /= 100; - break; - case MonsterPerks.PERK_LOSESTAT10: // pick a random stat to decrease by 10%, -3% XP - switch (rng.Between(0, 2)) - { - case 0: - enemy[i].hp *= 9; - enemy[i].hp /= 10; - break; - case 1: - enemy[i].damage *= 9; - enemy[i].damage /= 10; - break; - case 2: - enemy[i].absorb *= 9; - enemy[i].absorb /= 10; - break; - } - enemy[i].exp *= 97; - enemy[i].exp /= 100; - break; - case MonsterPerks.PERK_LOWRESIST: // pick a low resist or remove the corresponding weakness, +4% XP - switch (rng.Between(0, 3)) - { - case 0: - if ((enemy[i].elem_weakness & 0b10000000) == 0b10000000) - enemy[i].elem_weakness &= 0b01111111; - else - enemy[i].elem_resist |= 0b10000000; - break; - case 1: - if ((enemy[i].elem_weakness & 0b01000000) == 0b01000000) - enemy[i].elem_weakness &= 0b10111111; - else - enemy[i].elem_resist |= 0b01000000; - break; - case 2: - if ((enemy[i].elem_weakness & 0b00100000) == 0b00100000) - enemy[i].elem_weakness &= 0b11011111; - else - enemy[i].elem_resist |= 0b00100000; - break; - case 3: - if ((enemy[i].elem_weakness & 0b00010000) == 0b00001000) - enemy[i].elem_weakness &= 0b11101111; - else - enemy[i].elem_resist |= 0b00010000; - break; - } - enemy[i].exp *= 104; - enemy[i].exp /= 100; - break; - case MonsterPerks.PERK_LOWWEAKNESS: // pick a low weakness (or cancel a resist, or ignore for earth resist), -5% XP - switch (rng.Between(0, 3)) - { - case 0: - if ((enemy[i].elem_resist & 0b10000000) == 0b10000000) - didPerkRoll = false; - else - enemy[i].elem_weakness |= 0b10000000; - break; - case 1: - if ((enemy[i].elem_resist & 0b01000000) == 0b01000000) - enemy[i].elem_resist &= 0b10111111; - else - enemy[i].elem_weakness |= 0b01000000; - break; - case 2: - if ((enemy[i].elem_resist & 0b00100000) == 0b00100000) - enemy[i].elem_resist &= 0b11011111; - else - enemy[i].elem_weakness |= 0b00100000; - break; - case 3: - if ((enemy[i].elem_resist & 0b00010000) == 0b00010000) - enemy[i].elem_resist &= 0b11101111; - else - enemy[i].elem_weakness |= 0b00010000; - break; - } - if (didPerkRoll) - { - enemy[i].exp *= 95; - enemy[i].exp /= 100; - } - break; - case MonsterPerks.PERK_HIGHRESIST: // pick a high resist and remove the corresponding weakness, +3% XP - switch (rng.Between(0, 3)) - { - case 0: - enemy[i].elem_resist |= 0b00001000; - enemy[i].elem_weakness &= 0b11110111; - break; - case 1: - enemy[i].elem_resist |= 0b00000100; - enemy[i].elem_weakness &= 0b11111011; - break; - case 2: - enemy[i].elem_resist |= 0b00000010; - enemy[i].elem_weakness &= 0b11111101; - break; - case 3: - enemy[i].elem_resist |= 0b00000001; - enemy[i].elem_weakness &= 0b11111110; - break; - } - enemy[i].exp *= 103; - enemy[i].exp /= 100; - break; - case MonsterPerks.PERK_HIGHWEAKNESS: - switch (rng.Between(0, 3)) - { - case 0: - if ((enemy[i].elem_resist & 0b00001000) == 0b00001000) - enemy[i].elem_resist &= 0b11110111; - else - enemy[i].elem_weakness |= 0b00001000; - break; - case 1: - if ((enemy[i].elem_resist & 0b00000100) == 0b00000100) - enemy[i].elem_resist &= 0b11111011; - else - enemy[i].elem_weakness |= 0b00000100; - break; - case 2: - if ((enemy[i].elem_resist & 0b00000010) == 0b00000010) - enemy[i].elem_resist &= 0b11111101; - else - enemy[i].elem_weakness |= 0b00000010; - break; - case 3: - if ((enemy[i].elem_resist & 0b00000001) == 0b00000001) - enemy[i].elem_resist &= 0b11111110; - else - enemy[i].elem_weakness |= 0b00000001; - break; - } - if (didPerkRoll) - { - enemy[i].exp *= 97; - enemy[i].exp /= 100; - } - break; - case MonsterPerks.PERK_PLUSONEHIT: // +1 hit, +5/3/2/1... XP for an addition hit - enemy[i].num_hits++; - switch (enemy[i].num_hits) - { - case 2: - enemy[i].exp *= 21; - enemy[i].exp /= 20; - break; - case 3: - enemy[i].exp *= 103; - enemy[i].exp /= 100; - break; - case 4: - enemy[i].exp *= 51; - enemy[i].exp /= 50; - break; - default: - enemy[i].exp *= 101; - enemy[i].exp /= 100; - break; - } - break; - case MonsterPerks.PERK_POISONTOUCH: // adds poisontouch if enemy has no atk_ailment already, no XP increase - if (enemy[i].atk_ailment == 0) - { - enemy[i].atk_ailment = 0b00000100; - enemy[i].atk_elem = 0b00000010; - } - break; - case MonsterPerks.PERK_STUNSLEEPTOUCH: // adds stun or sleep touch if enemy has no atk_ailment already - if (enemy[i].atk_ailment == 0) - { - enemy[i].atk_ailment = rng.Between(0, 1) == 0 ? (byte)0b00100000 : (byte)0b00010000; - enemy[i].atk_elem = 0b00000001; - enemy[i].exp *= 103; - enemy[i].exp /= 100; - } - break; - case MonsterPerks.PERK_MUTETOUCH: // adds mute touch if enemy has no atk_ailment already - if (enemy[i].atk_ailment == 0) - { - enemy[i].atk_ailment = 0b01000000; - enemy[i].atk_elem = 0b00000001; - enemy[i].exp *= 51; - enemy[i].exp /= 50; - } - break; - } - num_perks++; - } + ApplyMinorPerks(rng, enemy[i], perks); } + // if enemy's vanilla name has already been used, add a name modifier - if (elemental == 9) // don't make alternate names for elementals - { - if (monsterBaseNameUsed[newEnemyImageLUT[enemy[i].image]]) - { - string nameModifier = monsterNameVariants[newEnemyImageLUT[enemy[i].image]].PickRandom(rng); - enemyNames[i] = nameModifier + enemyNames[i]; - monsterNameVariants[newEnemyImageLUT[enemy[i].image]].Remove(nameModifier); - } - else - monsterBaseNameUsed[newEnemyImageLUT[enemy[i].image]] = true; - } - } - if (enemy[i].AIscript != 0xFF) // set monster type to Mage if it has a script - enemy[i].monster_type |= 0b01000000; - for (int j = 1; j < enemy[i].num_hits; ++j) // for each hit past the first, reduce the base damage by one for every 15 points of damage rating - { - enemy[i].damage = enemy[i].damage - enemy[i].damage / 15; - } - if ((enemy[i].monster_type & 0b00001000) != 0) - { - enemy[i].elem_resist |= 0b00001000; // force death resist on undead enemies - enemy[i].elem_weakness &= 0b11110111; // and remove weakness to death - } - enemy[i].damage = rng.Between(enemy[i].damage - enemy[i].damage / 25, enemy[i].damage + enemy[i].damage / 25); // variance for damage rating - enemy[i].hp = rng.Between(enemy[i].hp - enemy[i].hp / 30, enemy[i].hp + enemy[i].hp / 30); // variance for hp rating - enemy[i].gp = rng.Between(enemy[i].gp - enemy[i].gp / 20, enemy[i].gp + enemy[i].gp / 20); // variance for gp reward - enemy[i].exp = rng.Between(enemy[i].exp - enemy[i].exp / 40, enemy[i].exp + enemy[i].exp / 40); // variance for exp reward - if(enemy[i].AIscript != 0xFF) - { - // determine skill tier - int highestTier = 0; - foreach(byte id in script[enemy[i].AIscript].skill_list) - { - if (id == 0xFF) - continue; - if (skill[id].tier > highestTier) - highestTier = skill[id].tier; - } - foreach(byte id in script[enemy[i].AIscript].spell_list) - { - if (id == 0xFF) - continue; - if (spell[id].tier > highestTier) - highestTier = spell[id].tier; - } - enemy[i].skilltier = highestTier; - } - } - } - for (int i = 0; i < GenericTilesetsCount; ++i) // remove palettes from tilesets where there are no mons using those palettes - { - List palRemoveList = new List { }; - foreach (byte pal in en.palettesInTileset[i]) - { - bool nopalettematch = true; - foreach (byte mon in en.enemiesInTileset[i]) - { - if (enemy[mon].pal == pal) + if (elemental == 9) { - nopalettematch = false; - break; + // don't make alternate names for elementals + ApplyNameModifier(i, enemy, enemyNames, newEnemyImageLUT, monsterBaseNameUsed, monsterNameVariants, rng); } } - if (nopalettematch) - { - palRemoveList.Add(pal); - } - } - foreach (byte pal in palRemoveList) - { - en.palettesInTileset[i].Remove(pal); + FinalizeEnemyStats(rng, enemy[i], script, skill, spell); } } + + // remove palettes from tilesets where there are no mons using those palettes + CleanupUnusedPalettes(en, enemy); + // modify scripts - // each spell is then selected from a list of spells available for that tier. it is not possible to promote to a higher tier + // each spell is then selected from a list of spells available for that tier. it is not possible to promote to a higher tier // skills will remain the same - for (int i = 0; i < script.Length - 10; ++i) // exclude the last 10 scripts - { - // start replacing each spell with another spell from the same tier - for (byte j = 0; j < 8; ++j) - { - if (script[i].spell_list[j] == 0xFF) - continue; // skip blank spells - int whichTier = spell[script[i].spell_list[j]].tier; - if (whichTier == 0) - whichTier = 1; // tier 0 becomes tier 1 - List eligibleSpellIDs = new List { }; - for (byte k = 0; k < 64; ++k) - { - if (spell[k].tier == whichTier) - eligibleSpellIDs.Add(k); - } - script[i].spell_list[j] = eligibleSpellIDs.PickRandom(rng); - } - } + ModifyScripts(rng, script, spell); + return true; } @@ -3486,7 +2437,7 @@ public void DoEnemizer(EnemyScripts enemyScripts, ZoneFormations zones, Flags fl { 3, 2, 3, 1, 2, 1, 4, 3, 3, 4, 4, 4, 5, 3, 4, 3, 4, 4, 4, 1, 5, 2, 2, 1, 5, 5 }; - for(int i = 0; i < EnemySkillCount; ++i) + for (int i = 0; i < EnemySkillCount; ++i) { skill[i] = new EnemySkillInfo(); skill[i].decompressData(Get(EnemySkillOffset + i * EnemySkillSize, EnemySkillSize)); @@ -3494,9 +2445,9 @@ public void DoEnemizer(EnemyScripts enemyScripts, ZoneFormations zones, Flags fl } EnemyInfo[] enemy = new EnemyInfo[EnemyCount]; // list of enemies, including information that is either inferred from formation inspection or tier lists that I have just made up EnemizerTrackingInfo en = new EnemizerTrackingInfo(); // structure that contains many lists and other information that is helpful for managing formation generation efficiently - // set enemy default tier list. these listings are based on a combination of where the enemy is placed in the game, its xp yield, its rough difficulty, whether it has a script or not, and gut feels - // -1 indicates a monster with special rules for enemy generation (usually a boss_ - int[] enemyTierList = new int[] { -1, 0, 0, 1, 1, 3, 1, 6, 5, 4, 6, 6, 0, 1, 4, -1, + // set enemy default tier list. these listings are based on a combination of where the enemy is placed in the game, its xp yield, its rough difficulty, whether it has a script or not, and gut feels + // -1 indicates a monster with special rules for enemy generation (usually a boss_ + int[] enemyTierList = new int[] { -1, 0, 0, 1, 1, 3, 1, 6, 5, 4, 6, 6, 0, 1, 4, -1, 1, 2, 5, 0, 7, 0, 3, 1, 2, 2, 4, 2, 2, 5, 1, 2, 5, 2, 5, 3, 4, 4, 5, 1, 2, 3, 5, 0, 1, 1, 2, 8, 5, 5, 7, -1, 4, 5, 4, 4, 4, 5, 3, 4, 5, 7, 1, 3, @@ -3528,7 +2479,7 @@ public void DoEnemizer(EnemyScripts enemyScripts, ZoneFormations zones, Flags fl { //byte[] patterntabledata = Get(EnemyPatternTablesOffset, 0x6800); // each pattern table is 0x800 bytes and there are 13 pattern tables that we will edit // do enemizer stuff - if(DoEnemizer_Enemies(rng, enemy, spell, skill, script.GetList().ToArray(), enemyNames, skillNames, shuffledSkillsOn, en)) + if (DoEnemizer_Enemies(rng, enemy, spell, skill, script.GetList().ToArray(), enemyNames, skillNames, shuffledSkillsOn, en)) { doFormations = true; // must use formation generator with enemizer for (int i = 0; i < EnemyCount; ++i) @@ -3546,16 +2497,16 @@ public void DoEnemizer(EnemyScripts enemyScripts, ZoneFormations zones, Flags fl throw new InsaneException("Something went wrong with Enemy Generation (Enemizer)!"); } } - if(doFormations) + if (doFormations) { if (!doEnemies) { //byte[] patterntabledata = Get(EnemyPatternTablesOffset, 0x6800); // each pattern table is 0x800 bytes and there are 13 pattern tables that we will edit DoEnemizer_EnemyPatternTablesOnly(rng, enemy, en); // rewrite the pattern tables and enemy palette assignments - //Put(EnemyPatternTablesOffset, patterntabledata); // write the new pattern tables as a chunk + //Put(EnemyPatternTablesOffset, patterntabledata); // write the new pattern tables as a chunk } - if(!DoEnemizer_Formations(rng, zones, enemy, en)) + if (!DoEnemizer_Formations(rng, zones, enemy, en)) { Console.WriteLine("Fission Mailed - Abort Formation Shuffle"); throw new InsaneException("Something went wrong with Formation Generation (Enemizer)!"); @@ -3594,8 +2545,8 @@ public void GenerateBalancedEnemyScripts(EnemyScripts enemyScripts, MT19337 rng, }*/ EnemyInfo[] enemy = new EnemyInfo[EnemyCount]; // list of enemies, including information that is either inferred from formation inspection or tier lists that I have just made up - // set enemy default tier list. these tier rankings are different from the enemizer basis, but show the kind of skills/spells that are prioritized - // when a script lands on that monster. the final 10 enemies are warmech, the fiends, and chaos. + // set enemy default tier list. these tier rankings are different from the enemizer basis, but show the kind of skills/spells that are prioritized + // when a script lands on that monster. the final 10 enemies are warmech, the fiends, and chaos. int[] enemyTierList = new int[] { -1, 1, 1, 1, 1, 2, 1, 3, 3, 2, 3, 3, 1, 1, 3, 1, 1, 1, 3, 1, 4, 1, 1, 1, 1, 1, 2, 1, 1, 3, 1, 1, 3, 1, 2, 2, 2, 2, 3, 1, 1, 2, 3, 1, 1, 1, 1, 4, @@ -3615,12 +2566,12 @@ public void GenerateBalancedEnemyScripts(EnemyScripts enemyScripts, MT19337 rng, bool[] scriptRepeat = new bool[script.Count()]; int[] scriptLowestTier = new int[script.Count()]; - for(int i = 0; i < script.Count(); ++i) + for (int i = 0; i < script.Count(); ++i) { scriptRepeat[i] = false; scriptLowestTier[i] = 10; } - for(int i = 1; i < EnemyCount - 10; ++i) + for (int i = 1; i < EnemyCount - 10; ++i) { if (enemy[i].AIscript == 0xFF) continue; // skip any enemy without a script @@ -3676,19 +2627,19 @@ public void GenerateBalancedEnemyScripts(EnemyScripts enemyScripts, MT19337 rng, break; } // cycle through skills, replacing each skill with a tier appropriate skill - for(byte j = 0; j < 4; ++j) + for (byte j = 0; j < 4; ++j) { if (script[enemy[i].AIscript].skill_list[j] == 0xFF) continue; // skip blank skills int diceRoll = rng.Between(0, 9); List eligibleSkillIDs = new List { }; int sumRoll = 0; - for(int k = 0; k < 4; ++k) + for (int k = 0; k < 4; ++k) { sumRoll += skilltierchance[k]; - if(diceRoll < sumRoll) + if (diceRoll < sumRoll) { - for(byte l = 0; l < EnemySkillCount; ++l) + for (byte l = 0; l < EnemySkillCount; ++l) { if (skill[l].tier == k + 1) eligibleSkillIDs.Add(l); @@ -3707,15 +2658,15 @@ public void GenerateBalancedEnemyScripts(EnemyScripts enemyScripts, MT19337 rng, List eligibleSpellIDs = new List { }; int bestTier = 1; int bestValue = -1; - for(int j = 0; j < 5; ++j) + for (int j = 0; j < 5; ++j) { - if(tierchance[j] > bestValue) + if (tierchance[j] > bestValue) { bestValue = tierchance[j]; bestTier = j + 1; } } - for(byte j = 0; j < MagicCount; ++j) + for (byte j = 0; j < MagicCount; ++j) { if (spell[j].tier == bestTier) eligibleSpellIDs.Add(j); @@ -3766,60 +2717,6 @@ public SpellInfo[] LoadSpells(int count = MagicCount) return spell; } - public void ObfuscateEnemies(MT19337 rng, Flags flags) - { - var flagsValue = EnemyObfuscation.None; - - if (flagsValue == EnemyObfuscation.Imp || flagsValue == EnemyObfuscation.ImpAll) - { - List formations = LoadFormations(); - var enemyNames = EnemyText; - - 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" }; - - var names = alphabet.Join(alphabet, x => true, x => true, (a, b) => a + b.ToLower() + "IMP").ToList(); - - List<(string name, byte pal)> variants = new List<(string, byte)>(); - for (int i = 0; i < 256; i++) - { - var name = names.SpliceRandom(rng); - variants.Add((name, (byte)rng.Between(0, 128))); - } - - int limit = flagsValue == EnemyObfuscation.ImpAll ? 128 : 119; - - for (int i = 0; i < limit; i++) enemyNames[i] = variants[i].name; - - for (int i = 0; i < FormationCount; ++i) - { - if (formations[i].id.Any(id => id >= limit)) continue; - - formations[i].pics = 0; - formations[i].shape = 0; - formations[i].tileset = 0; - formations[i].pal1 = variants[formations[i].id[0]].pal; - formations[i].pal1 = variants[formations[i].id[1]].pal; - - if(flagsValue == EnemyObfuscation.ImpAll && (flags.TrappedChaos ?? false) && formations[i].id.Any(id => id == 127)) - { - formations[i].unrunnable_a = false; - formations[i].unrunnable_b = false; - } - } - - StoreFormations(formations); - } - } - - - private void StoreFormations(List formations) - { - for (int i = 0; i < FormationCount; ++i) - { - Put(FormationDataOffset + i * FormationSize, formations[i].compressData()); - } - } - private List LoadFormations() { List formations = new List(); @@ -3833,18 +2730,982 @@ private List LoadFormations() return formations; } - } + private void HandleSpecialMonster(byte i, EnemyInfo[] enemy, string[] enemyNames, List newEnemyImageLUT) + { + switch (i) + { + case Enemy.Imp: + enemyNames[i] = "CHAMP"; + enemy[i].elem_weakness = 0b11111111; + enemy[i].monster_type = 0b01111111; + break; + case Enemy.Pirate: + // image forced to where the PIRATE image was moved + enemy[i].image = (byte)newEnemyImageLUT.IndexOf(6); + enemy[i].pal = 0x0B; + break; + case Enemy.Phantom: + break; + case Enemy.Garland: + // image forced to where the GARLAND image was moved + enemy[i].image = (byte)newEnemyImageLUT.IndexOf(46); + enemy[i].pal = 0x13; + break; + case Enemy.Astos: + // image forced to where the ASTOS image was moved + enemy[i].image = (byte)newEnemyImageLUT.IndexOf(50); + enemy[i].pal = 0x06; + break; + case Enemy.WarMech: + enemyNames[i] = GetWarMechName(newEnemyImageLUT[enemy[i].image]); + break; + } + } - public enum EnemyObfuscation - { - [Description("None")] - None, + private string GetWarMechName(byte imageType) + { + var warMechNames = new Dictionary + { + {0, "WarIMP"}, {1, "WarAGAMA"}, {2, "WarWOLF"}, {3, "WarGIANT"}, + {4, "WarSAHAG"}, {5, "WarSHARK"}, {6, "WarBRUTE"}, {7, "WarEYE"}, + {8, "WarBONE"}, {9, "WarHYENA"}, {10, "WarCREEP"}, {11, "WarOGRE"}, + {12, "WarSNAKE"}, {13, "WarBULL"}, {14, "WarLBSTR"}, {15, "WarTROLL"}, + {16, "WarGHOST"}, {17, "WarWORM"}, {18, "WarGEIST"}, {19, "WarEYE"}, + {20, "WarLAMIA"}, {21, "WarPEDE"}, {22, "WarCAT"}, {23, "WarTIGER"}, + {24, "WarVAMP"}, {25, "WarDJINN"}, {26, "WarGOYLE"}, {27, "WarDRAKE"}, + {28, "WarOOZE"}, {29, "WarSPHNX"}, {30, "WarBUG"}, {31, "WarTURTL"}, + {32, "WarMUMMY"}, {33, "WarWYRM"}, {34, "WarBIRD"}, {35, "WarREX"}, + {36, "WarFISH"}, {37, "WarOCHO"}, {38, "WarGATOR"}, {39, "WarHYDRA"}, + {40, "WarMECH"}, {41, "WarNAGA"}, {42, "WarWATER"}, {43, "WarBEAST"}, + {44, "WarPISCO"}, {45, "WarDRAKE"}, {46, "WarGRLND"}, {47, "WarGOLEM"}, + {48, "WarMAN"}, {49, "WarPONY"}, {50, "WarELF"}, {51, "WarMECH"} + }; + + return warMechNames.TryGetValue(imageType, out string name) ? name : "WarMECH"; + } + + private void ConfigureEnemyByImage(MT19337 rng, byte i, EnemyInfo[] enemy, string[] enemyNames, + List newEnemyImageLUT, ref int elemental, List elementalsSelected, List dragonsSelected) + { + byte imageType = newEnemyImageLUT[enemy[i].image]; + + var config = GetEnemyConfiguration(imageType); + enemyNames[i] = config.Name; + enemy[i].monster_type = config.MonsterType; + enemy[i].elem_resist = config.ElemResist; + enemy[i].elem_weakness = config.ElemWeakness; + + if (config.CritRateRange.HasValue) + { + var (min, max) = config.CritRateRange.Value; + enemy[i].critrate = rng.Between(min, max); + } + + if (config.IsLargeElemental) + { + if (elementalsSelected.Count > 0) + { + elemental = elementalsSelected.PickRandom(rng); + } + ENE_rollLargeElemental(rng, ref enemy[i], elemental); + enemyNames[i] = GetElementalName(elemental, true); + if (elemental != 9) + { + elementalsSelected.Remove(elemental); + } + } + else if (config.IsSmallElemental) + { + if (elementalsSelected.Count > 0) + { + elemental = elementalsSelected.PickRandom(rng); + } + ENE_rollSmallElemental(rng, ref enemy[i], elemental); + enemyNames[i] = GetElementalName(elemental, false); + if (elemental != 9) + { + elementalsSelected.Remove(elemental); + } + } + else if (config.IsDragon) + { + if (dragonsSelected.Count > 0) + { + elemental = dragonsSelected.PickRandom(rng); + } + ENE_rollDragon(rng, ref enemy[i], elemental); + enemyNames[i] = GetDragonName(elemental, imageType == 27); + if (elemental != 9) + { + dragonsSelected.Remove(elemental); + } + } + + if (config.SpecialAbsorbChance) + { + // 50% chance of max absorb for this enemy type + if (rng.Between(0, 1) == 1) + { + enemy[i].absorb = 255; + } + } + + if (config.SpecialGolemLogic) + { + ApplyGolemResistLogic(rng, enemy[i]); + } + + if (config.SpecialFlanLogic) + { + ApplyFlanWeaknessLogic(rng, enemy[i]); + } + } + + private EnemizerVariant GetEnemyConfiguration(byte imageType) + { + var configs = new Dictionary + { + // Imp + {0, new EnemizerVariant("IMP", 0b00000100, 0, 0)}, + // Iguana + {1, new EnemizerVariant("SAUR", 0b00000010, 0, 0)}, + // Wolf + {2, new EnemizerVariant("WOLF", 0b00000000, 0, 0)}, + // Giant + {3, new EnemizerVariant("GIANT", 0b00000100, 0, 0)}, + // Sahag + {4, new EnemizerVariant("SAHAG", 0b00100000, 0b10010000, 0b01000000)}, + // Shark + {5, new EnemizerVariant("SHARK", 0b00100000, 0b10010000, 0b01000000)}, + // Pirate + {6, new EnemizerVariant("BRUTE", 0b00000000, 0b10000000, 0)}, + // Oddeye + {7, new EnemizerVariant("EYES", 0b00100000, 0b10010000, 0b01000000)}, + // Bone + {8, new EnemizerVariant("BONE", 0b00001000, 0b00101011, 0b00010000)}, + // Hyena + {9, new EnemizerVariant("DOG", 0b00000000, 0, 0)}, + // Creep + {10, new EnemizerVariant("CRAWL", 0b00000000, 0, 0b00010000)}, + // Ogre + {11, new EnemizerVariant("OGRE", 0b00000100, 0, 0)}, + // Asp + {12, new EnemizerVariant("ASP", 0b00000010, 0, 0) { CritRateRange = (1, 20) }}, + // Bull + {13, new EnemizerVariant("BULL", 0b00000000, 0, 0)}, + // Crab + {14, new EnemizerVariant("CRAB", 0b00000000, 0, 0)}, + // Troll + {15, new EnemizerVariant("TROLL", 0b10000000, 0, 0)}, + // Spectral Undead / Ghost + {16, new EnemizerVariant("GHOST", 0b00001001, 0b10101011, 0b00010000)}, + // Worm + {17, new EnemizerVariant("WORM", 0b00000000, 0b10000000, 0)}, + // Zombie Undead / Wight + {18, new EnemizerVariant("WIGHT", 0b00001000, 0b00101011, 0b00010000)}, + // Eye + {19, new EnemizerVariant("EYE", 0b01000000, 0b10000000, 0)}, + // Medusa + {20, new EnemizerVariant("LAMIA", 0b00000000, 0, 0)}, + // Pede + {21, new EnemizerVariant("PEDE", 0b00000000, 0, 0)}, + // Were / Catman + {22, new EnemizerVariant("CAT", 0b00010000, 0, 0)}, + // Tiger + {23, new EnemizerVariant("TIGER", 0b00000000, 0, 0) { CritRateRange = (20, 80) }}, + // Vampire + {24, new EnemizerVariant("VAMP", 0b10001001, 0b10101011, 0b00010000)}, + // Large Elemental + {25, new EnemizerVariant("", 0, 0, 0) { IsLargeElemental = true }}, + // Gargoyle / Goyle + {26, new EnemizerVariant("GOYLE", 0b00000001, 0b10000000, 0)}, + // Drake 1 / Dragon Type 1 + {27, new EnemizerVariant("", 0, 0, 0) { IsDragon = true }}, + // Slime / Flan + {28, new EnemizerVariant("FLAN", 0b00000001, 0, 0) { SpecialFlanLogic = true, SpecialAbsorbChance = true }}, + // Manticore + {29, new EnemizerVariant("MANT", 0b00000000, 0, 0)}, + // Spider + {30, new EnemizerVariant("BUG", 0b00000000, 0, 0)}, + // Ankylo + {31, new EnemizerVariant("ANK", 0b00000000, 0, 0)}, + // Mummy + {32, new EnemizerVariant("MUMMY", 0b00001000, 0b00101011, 0b00010000)}, + // Wyvern + {33, new EnemizerVariant("WYRM", 0b00000010, 0b10000000, 0)}, + // Jerk Bird / Bird + {34, new EnemizerVariant("BIRD", 0b00000000, 0b10000000, 0)}, + // Steak / Tyro + {35, new EnemizerVariant("TYRO", 0b00000010, 0, 0) { CritRateRange = (20, 80) }}, + // Pirahna / Caribe + {36, new EnemizerVariant("FISH", 0b00100000, 0b10010000, 0b01000000)}, + // Ocho + {37, new EnemizerVariant("OCHO", 0b00100000, 0b10010000, 0b01000000)}, + // Gator + {38, new EnemizerVariant("GATOR", 0b00100010, 0b10010000, 0b01000000)}, + // Hydra + {39, new EnemizerVariant("HYDRA", 0b00000010, 0, 0)}, + // Robot / Bot + {40, new EnemizerVariant("BOT", 0b00000000, 0b00001011, 0b01000000)}, + // Naga + {41, new EnemizerVariant("NAGA", 0b01000000, 0, 0)}, + // Small Elemental + {42, new EnemizerVariant("AIR", 0, 0, 0) { IsSmallElemental = true }}, + // Chimera + {43, new EnemizerVariant("BEAST", 0b00000010, 0b10010000, 0b00100000)}, + // Piscodemon + {44, new EnemizerVariant("SQUID", 0b00000000, 0b00110011, 0)}, + // Dragon 2 / Dragon Type 2 + {45, new EnemizerVariant("", 0, 0, 0) { IsDragon = true }}, + // Knight Type 1 + {46, new EnemizerVariant("KNIGHT", 0b00000000, 0, 0)}, + // Golem + {47, new EnemizerVariant("GOLEM", 0b00000000, 0b01111011, 0) { SpecialGolemLogic = true }}, + // Knight Type 2 / Badman + {48, new EnemizerVariant("RANGER", 0b00000000, 0, 0)}, + // Pony + {49, new EnemizerVariant("PONY", 0b00000000, 0, 0)}, + // Elf + {50, new EnemizerVariant("ELF", 0b01000000, 0, 0)}, + // War Machine / Mech + {51, new EnemizerVariant("MECH", 0b10000000, 0b00111011, 0b01000000)} + }; + + return configs.TryGetValue(imageType, out var config) ? config : new EnemizerVariant("UNKNOWN", 0, 0, 0); + } + + private string GetElementalName(int elemental, bool isLarge) + { + var names = new Dictionary + { + {1, ("EARTH", "SHARD")}, {2, ("STORM", "WIND")}, {3, ("ICE", "WATER")}, + {4, ("FIRE", "FLARE")}, {5, ("DEATH", "DOOM")}, {6, ("CRONO", "TIME")}, + {7, ("VENOM", "BANE")}, {8, ("DJINN", "MAGIC")} + }; + + if (names.TryGetValue(elemental, out var namePair)) + { + return isLarge ? namePair.large : namePair.small; + } + + return isLarge ? "FORCE" : "AIR"; + } + + private string GetDragonName(int elemental, bool isDrake) + { + var names = new Dictionary + { + {1, "Earth D"}, {2, "Elec D"}, {3, "Ice D"}, {4, "Fire D"}, + {5, "Death D"}, {6, "Time D"}, {7, "Gas D"}, {8, "Magic D"} + }; + + if (names.TryGetValue(elemental, out var name)) + { + return name; + } + + return isDrake ? "DRAKE" : "DRAGON"; + } + + private void ApplyGolemResistLogic(MT19337 rng, EnemyInfo enemy) + { + if (enemy.absorb > (enemy.tier < 3 ? 30 : 60)) + { + int resistToRemove = rng.Between(0, 2); + byte mask = resistToRemove switch + { + 0 => 0b10111111, + 1 => 0b11011111, + _ => 0b11101111 + }; + enemy.elem_resist &= mask; + } + } + + private void ApplyFlanWeaknessLogic(MT19337 rng, EnemyInfo enemy) + { + // can be fire, ice, fire+ice, lit, fire+lit, or ice+lit weak + enemy.elem_weakness = (byte)(rng.Between(1, 6) << 4); + // resist all other elements except time + enemy.elem_resist = (byte)((0b11111111 ^ enemy.elem_weakness) & 0b11111011); + } + + private bool ApplyGlobalPerk(byte enemyIndex, EnemyInfo[] enemy, MT19337 rng) + { + if (enemyIndex == Enemy.Crawl) + { + // Global Perk - multi-hitter with weak attacks but stun touch guaranteed (overrides existing atk ailment) + enemy[enemyIndex].atk_elem = 0b00000001; + enemy[enemyIndex].atk_ailment = 0b00010000; + enemy[enemyIndex].num_hits = 8; + enemy[enemyIndex].damage = 1; + return true; + } + + if (enemyIndex == Enemy.Coctrice) + { + // Global Perk - stonetouch (overrides other atk_elem and ailment), -1 hit if num_hits > 1 + enemy[enemyIndex].atk_elem = 0b00000010; + enemy[enemyIndex].atk_ailment = 0b00000010; + if (enemy[enemyIndex].num_hits > 1) + { + enemy[enemyIndex].num_hits--; + } + return true; + } + + if (enemyIndex == Enemy.Sorcerer) + { + // Global Perk - deathtouch + enemy[enemyIndex].atk_elem = 0b00000000; + enemy[enemyIndex].atk_ailment = 0b00000001; + return true; + } + + return false; + } + + private bool TryApplyClassVariant(MT19337 rng, byte i, EnemyInfo[] enemy, string[] enemyNames, + List newEnemyImageLUT, List[] monsterClassVariants, bool shuffledSkillsOn) + { + byte imageType = newEnemyImageLUT[enemy[i].image]; + var variants = monsterClassVariants[imageType]; + + if (rng.Between(0, 11) >= variants.Count) + { + return true; + } + + string classModifier = variants.PickRandom(rng); + // apply the traits of this enemy class + ApplyClassModifierEffects(classModifier, enemy[i], shuffledSkillsOn, rng); + // change the enemy's name + enemyNames[i] = classModifier + enemyNames[i]; + // remove this variant from the list of variants available for this enemy image + variants.Remove(classModifier); + + return false; + } + + private void ApplyClassModifierEffects(string modifier, EnemyInfo enemy, bool shuffledSkillsOn, MT19337 rng) + { + switch (modifier) + { + case "Wz": + if (enemy.AIscript == 0xFF && !shuffledSkillsOn) + { + enemy.AIscript = ENE_PickForcedAIScript(enemy.tier, rng); + } + break; + + case "Fr": + enemy.elem_resist |= 0b00100000; + enemy.elem_resist &= 0b11101111; + enemy.elem_weakness |= 0b00010000; + enemy.elem_weakness &= 0b11011111; + enemy.atk_elem = 0b00100000; + break; + + case "R.": + enemy.elem_resist |= 0b00010000; + enemy.elem_resist &= 0b11011111; + enemy.elem_weakness |= 0b00100000; + enemy.elem_weakness &= 0b11101111; + enemy.atk_elem = 0b00010000; + break; + + case "Z.": + enemy.elem_resist |= 0b00101011; + enemy.elem_resist &= 0b11101111; + enemy.elem_weakness |= 0b00010000; + enemy.elem_weakness &= 0b11010100; + enemy.monster_type |= 0b00001000; + if (enemy.atk_ailment == 0) + { + enemy.atk_ailment = 0b00010000; + enemy.atk_elem = 0b00000001; + } + break; + + case "Sea": + enemy.elem_resist |= 0b10010000; + enemy.elem_resist &= 0b10111111; + enemy.elem_weakness |= 0b01000000; + enemy.elem_weakness &= 0b01101111; + enemy.monster_type |= 0b00100000; + break; + + case "Wr": + enemy.atk_ailment = 0b00000100; + enemy.atk_elem = 0b00000010; + enemy.monster_type |= 0b10010000; + break; + } + } + + private void ApplyMinorPerks(MT19337 rng, EnemyInfo enemy, List perks) + { + int numPerks = 0; + while (rng.Between(0, 7) > 1 + numPerks && numPerks < 4) + { + // select from the list of available perks + MonsterPerks perk = perks.PickRandom(rng); + bool appliedSuccessfully = ApplySinglePerk(rng, enemy, perk); + if (appliedSuccessfully) + { + numPerks++; + } + } + } + + private bool ApplySinglePerk(MT19337 rng, EnemyInfo enemy, MonsterPerks perk) + { + switch (perk) + { + case MonsterPerks.PERK_GAINSTAT10: + return ApplyGainStatPerk(rng, enemy); + + case MonsterPerks.PERK_LOSESTAT10: + return ApplyLoseStatPerk(rng, enemy); + + case MonsterPerks.PERK_LOWRESIST: + return ApplyLowResistPerk(rng, enemy); + + case MonsterPerks.PERK_LOWWEAKNESS: + return ApplyLowWeaknessPerk(rng, enemy); + + case MonsterPerks.PERK_HIGHRESIST: + return ApplyHighResistPerk(rng, enemy); + + case MonsterPerks.PERK_HIGHWEAKNESS: + return ApplyHighWeaknessPerk(rng, enemy); + + case MonsterPerks.PERK_PLUSONEHIT: + return ApplyPlusOneHitPerk(enemy); + + case MonsterPerks.PERK_POISONTOUCH: + return ApplyPoisonTouchPerk(enemy); + + case MonsterPerks.PERK_STUNSLEEPTOUCH: + return ApplyStunSleepTouchPerk(rng, enemy); + + case MonsterPerks.PERK_MUTETOUCH: + return ApplyMuteTouchPerk(enemy); + + default: + return false; + } + } + + private bool ApplyGainStatPerk(MT19337 rng, EnemyInfo enemy) + { + // pick a random stat to increase by 10%, +3% XP + int statChoice = rng.Between(0, 2); + switch (statChoice) + { + case 0: + enemy.hp = enemy.hp * 11 / 10; + break; + case 1: + enemy.damage = enemy.damage * 11 / 10; + break; + case 2: + enemy.absorb = enemy.absorb * 11 / 10; + break; + } + enemy.exp = enemy.exp * 103 / 100; + return true; + } + + private bool ApplyLoseStatPerk(MT19337 rng, EnemyInfo enemy) + { + // pick a random stat to decrease by 10%, -3% XP + int statChoice = rng.Between(0, 2); + switch (statChoice) + { + case 0: + enemy.hp = enemy.hp * 9 / 10; + break; + case 1: + enemy.damage = enemy.damage * 9 / 10; + break; + case 2: + enemy.absorb = enemy.absorb * 9 / 10; + break; + } + enemy.exp = enemy.exp * 97 / 100; + return true; + } + + private bool ApplyLowResistPerk(MT19337 rng, EnemyInfo enemy) + { + // pick a low resist or remove the corresponding weakness, +4% XP + var elementBits = new[] { 0b10000000, 0b01000000, 0b00100000, 0b00010000 }; + byte elementBit = (byte)elementBits[rng.Between(0, 3)]; + + if ((enemy.elem_weakness & elementBit) == elementBit) + { + enemy.elem_weakness &= (byte)~elementBit; + } + else + { + enemy.elem_resist |= elementBit; + } + + enemy.exp = enemy.exp * 104 / 100; + return true; + } + + private bool ApplyLowWeaknessPerk(MT19337 rng, EnemyInfo enemy) + { + // pick a low weakness (or cancel a resist, or ignore for earth resist), -5% XP + var elementBits = new[] { 0b10000000, 0b01000000, 0b00100000, 0b00010000 }; + byte elementBit = (byte)elementBits[rng.Between(0, 3)]; + + // Earth resist special case + if ((enemy.elem_resist & elementBit) == elementBit && elementBit == 0b10000000) + { + return false; + } + + if ((enemy.elem_resist & elementBit) == elementBit) + { + enemy.elem_resist &= (byte)~elementBit; + } + else + { + enemy.elem_weakness |= elementBit; + } + + enemy.exp = enemy.exp * 95 / 100; + return true; + } + + private bool ApplyHighResistPerk(MT19337 rng, EnemyInfo enemy) + { + // pick a high resist and remove the corresponding weakness, +3% XP + var resistAndWeaknessPairs = new[] + { + (resist: 0b00001000, weakness: 0b11110111), + (resist: 0b00000100, weakness: 0b11111011), + (resist: 0b00000010, weakness: 0b11111101), + (resist: 0b00000001, weakness: 0b11111110) + }; + + var pair = resistAndWeaknessPairs[rng.Between(0, 3)]; + enemy.elem_resist |= (byte)pair.resist; + enemy.elem_weakness &= (byte)pair.weakness; + enemy.exp = enemy.exp * 103 / 100; + return true; + } + + private bool ApplyHighWeaknessPerk(MT19337 rng, EnemyInfo enemy) + { + // pick a high weakness (or cancel a resist), -3% XP + var elementBits = new[] { 0b00001000, 0b00000100, 0b00000010, 0b00000001 }; + byte elementBit = (byte)elementBits[rng.Between(0, 3)]; + + if ((enemy.elem_resist & elementBit) == elementBit) + { + enemy.elem_resist &= (byte)~elementBit; + } + else + { + enemy.elem_weakness |= elementBit; + } + + enemy.exp = enemy.exp * 97 / 100; + return true; + } + + private bool ApplyPlusOneHitPerk(EnemyInfo enemy) + { + // +1 hit, +5/3/2/1... XP for an addition hit + enemy.num_hits++; + + int expMultiplier = enemy.num_hits switch + { + 2 => 105, + 3 => 103, + 4 => 102, + _ => 101 + }; + + enemy.exp = enemy.exp * expMultiplier / 100; + return true; + } + + private bool ApplyPoisonTouchPerk(EnemyInfo enemy) + { + // adds poisontouch if enemy has no atk_ailment already, no XP increase + if (enemy.atk_ailment != 0) + { + return false; + } + + enemy.atk_ailment = 0b00000100; + enemy.atk_elem = 0b00000010; + return true; + } + + private bool ApplyStunSleepTouchPerk(MT19337 rng, EnemyInfo enemy) + { + // adds stun or sleep touch if enemy has no atk_ailment already, +3% XP + if (enemy.atk_ailment != 0) + { + return false; + } + + enemy.atk_ailment = rng.Between(0, 1) == 0 ? (byte)0b00100000 : (byte)0b00010000; + enemy.atk_elem = 0b00000001; + enemy.exp = enemy.exp * 103 / 100; + return true; + } + + private bool ApplyMuteTouchPerk(EnemyInfo enemy) + { + // adds mute touch if enemy has no atk_ailment already, +2% XP + if (enemy.atk_ailment != 0) + { + return false; + } + + enemy.atk_ailment = 0b01000000; + enemy.atk_elem = 0b00000001; + enemy.exp = enemy.exp * 102 / 100; + return true; + } + + private void ApplyNameModifier(byte i, EnemyInfo[] enemy, string[] enemyNames, List newEnemyImageLUT, + bool[] monsterBaseNameUsed, List[] monsterNameVariants, MT19337 rng) + { + byte imageType = newEnemyImageLUT[enemy[i].image]; + + if (monsterBaseNameUsed[imageType]) + { + string nameModifier = monsterNameVariants[imageType].PickRandom(rng); + enemyNames[i] = nameModifier + enemyNames[i]; + monsterNameVariants[imageType].Remove(nameModifier); + } + else + { + monsterBaseNameUsed[imageType] = true; + } + } + + private void FinalizeEnemyStats(MT19337 rng, EnemyInfo enemy, EnemyScriptInfo[] script, + EnemySkillInfo[] skill, SpellInfo[] spell) + { + // set monster type to Mage if it has a script + if (enemy.AIscript != 0xFF) + { + enemy.monster_type |= 0b01000000; + } + + // for each hit past the first, reduce the base damage by one for every 15 points of damage rating + for (int j = 1; j < enemy.num_hits; ++j) + { + enemy.damage = enemy.damage - enemy.damage / 15; + } + + // force death resist on undead enemies and remove weakness to death + if ((enemy.monster_type & 0b00001000) != 0) + { + enemy.elem_resist |= 0b00001000; + enemy.elem_weakness &= 0b11110111; + } + + // variance for damage rating + enemy.damage = rng.Between(enemy.damage - enemy.damage / 25, enemy.damage + enemy.damage / 25); + // variance for hp rating + enemy.hp = rng.Between(enemy.hp - enemy.hp / 30, enemy.hp + enemy.hp / 30); + // variance for gp reward + enemy.gp = rng.Between(enemy.gp - enemy.gp / 20, enemy.gp + enemy.gp / 20); + // variance for exp reward + enemy.exp = rng.Between(enemy.exp - enemy.exp / 40, enemy.exp + enemy.exp / 40); + + if (enemy.AIscript != 0xFF) + { + // determine skill tier + enemy.skilltier = DetermineSkillTier(enemy, script, skill, spell); + } + } + + private int DetermineSkillTier(EnemyInfo enemy, EnemyScriptInfo[] script, + EnemySkillInfo[] skill, SpellInfo[] spell) + { + int highestTier = 0; + + foreach (byte id in script[enemy.AIscript].skill_list) + { + if (id == 0xFF) + continue; + if (skill[id].tier > highestTier) + highestTier = skill[id].tier; + } + + foreach (byte id in script[enemy.AIscript].spell_list) + { + if (id == 0xFF) + continue; + if (spell[id].tier > highestTier) + highestTier = spell[id].tier; + } + + return highestTier; + } + + private void AdjustStatsForSizeChange(EnemyInfo enemy, bool oldSize) + { + if (oldSize && enemy.Small) + { + // large became small + enemy.hp = enemy.hp * 4 / 5; + enemy.exp = enemy.exp * 9 / 10; + enemy.gp = enemy.gp * 9 / 10; + } + else if (!oldSize && enemy.Large) + { + // small became large + enemy.hp = enemy.hp * 5 / 4; + enemy.exp = enemy.exp * 10 / 9; + enemy.gp = enemy.gp * 10 / 9; + } + } + + private void AddEligiblePerks(List perks) + { + perks.Add(MonsterPerks.PERK_LOWRESIST); + perks.Add(MonsterPerks.PERK_HIGHRESIST); + perks.Add(MonsterPerks.PERK_LOWWEAKNESS); + perks.Add(MonsterPerks.PERK_HIGHWEAKNESS); + perks.Add(MonsterPerks.PERK_PLUSONEHIT); + perks.Add(MonsterPerks.PERK_POISONTOUCH); + perks.Add(MonsterPerks.PERK_STUNSLEEPTOUCH); + perks.Add(MonsterPerks.PERK_MUTETOUCH); + } + + private void CleanupUnusedPalettes(EnemizerTrackingInfo en, EnemyInfo[] enemy) + { + for (int i = 0; i < GenericTilesetsCount; ++i) + { + List palRemoveList = new List(); + + foreach (byte pal in en.palettesInTileset[i]) + { + bool nopalettematch = true; + foreach (byte mon in en.enemiesInTileset[i]) + { + if (enemy[mon].pal == pal) + { + nopalettematch = false; + break; + } + } + if (nopalettematch) + { + palRemoveList.Add(pal); + } + } + + foreach (byte pal in palRemoveList) + { + en.palettesInTileset[i].Remove(pal); + } + } + } + + private void ModifyScripts(MT19337 rng, EnemyScriptInfo[] script, SpellInfo[] spell) + { + // exclude the last 10 scripts + for (int i = 0; i < script.Length - 10; ++i) + { + // start replacing each spell with another spell from the same tier + for (byte j = 0; j < 8; ++j) + { + // skip blank spells + if (script[i].spell_list[j] == 0xFF) + continue; + + int tier = spell[script[i].spell_list[j]].tier; + // tier 0 becomes tier 1 + if (tier == 0) + tier = 1; + + List eligibleSpellIDs = new List(); + for (byte k = 0; k < 64; ++k) + { + if (spell[k].tier == tier) + eligibleSpellIDs.Add(k); + } + + script[i].spell_list[j] = eligibleSpellIDs.PickRandom(rng); + } + } + } + + private void InitializeTilesetPalettes(MT19337 rng, EnemizerTrackingInfo en) + { + for (int i = 0; i < GenericTilesetsCount; ++i) + { + en.palettesInTileset[i].Clear(); + while (en.palettesInTileset[i].Count < 4) + { + int newPal = rng.Between(0, 0x3F); + if (!en.palettesInTileset[i].Contains((byte)newPal)) + { + en.palettesInTileset[i].Add((byte)newPal); + } + } + } + } + + private void ClearEnemiesInTilesets(EnemizerTrackingInfo en) + { + for (int i = 0; i < GenericTilesetsCount; ++i) + en.enemiesInTileset[i].Clear(); + } + + private List[] InitializeMonsterClassVariants() + { + var variants = new List[52]; + + // Imp + variants[0] = new List { "Fr", "R.", "Z.", "Wr", "Wz", "Sea" }; + // Iguana + variants[1] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; + // Wolf + variants[2] = new List { "Fr", "R.", "Z.", "Wr", "Wz" }; + // Giant + variants[3] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; + // Sahag + variants[4] = new List { "Fr", "Z.", "Wr", "Wz" }; + // Shark + variants[5] = new List { "Fr", "Z." }; + // Pirate + variants[6] = new List { "Fr", "R.", "Z.", "Wz" }; + // Oddeye + variants[7] = new List { "Fr", "Z." }; + // Bone + variants[8] = new List { "Wz" }; + // Hyena + variants[9] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; + // Creep + variants[10] = new List { "Fr", "R.", "Z.", "Wr", "Wz", "Sea" }; + // Ogre + variants[11] = new List { "Fr", "R.", "Z.", "Wr" }; + // Asp + variants[12] = new List { "Fr", "R.", "Z.", "Sea" }; + // Bull + variants[13] = new List { "Fr", "R.", "Z.", "Wr", "Wz", "Sea" }; + // Crab + variants[14] = new List { "Fr", "R.", "Z.", "Sea" }; + // Troll + variants[15] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; + // Spectral Undead / Ghost + variants[16] = new List { "Wz" }; + // Worm + variants[17] = new List { "Fr", "R.", "Z.", "Sea" }; + // Zombie Undead / Wight + variants[18] = new List { "Sea" }; + // Eye + variants[19] = new List { "Fr", "R.", "Z.", "Sea" }; + // Medusa + variants[20] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; + // Pede + variants[21] = new List { "Fr", "R.", "Z.", "Sea" }; + // Were / Catman + variants[22] = new List { "Fr", "R.", "Z.", "Wz" }; + // Tiger + variants[23] = new List { "Fr", "R.", "Z.", "Wr" }; + // Vampire + variants[24] = new List { "Wz" }; + // Large Elemental + variants[25] = new List { }; + // Gargoyle / Goyle + variants[26] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; + // Drake 1 / Dragon Type 1 + variants[27] = new List { }; + // Slime / Flan + variants[28] = new List { "Wz" }; + // Manticore + variants[29] = new List { "Fr", "R.", "Z.", "Wz", "Sea" }; + // Spider + variants[30] = new List { "Fr", "R.", "Z.", "Sea" }; + // Ankylo + variants[31] = new List { "Fr", "R.", "Z." }; + // Mummy + variants[32] = new List { "Wz" }; + // Wyvern + variants[33] = new List { "Fr", "R.", "Z.", "Wr", "Wz" }; + // Jerk Bird / Bird + variants[34] = new List { "Fr", "R.", "Z." }; + // Steak / Tyro + variants[35] = new List { "Fr", "R.", "Z.", "Sea" }; + // Pirahna / Caribe + variants[36] = new List { "Fr", "Z." }; + // Ocho + variants[37] = new List { "Fr", "Z.", "Wz" }; + // Gator + variants[38] = new List { "Fr", "Z.", "Wr" }; + // Hydra + variants[39] = new List { "Fr", "R.", "Z.", "Sea" }; + // Robot / Bot + variants[40] = new List { }; + // Naga + variants[41] = new List { "Fr", "Z.", "Sea" }; + // Small Elemental + variants[42] = new List { }; + // Chimera + variants[43] = new List { "Z.", "Wr", "Wz" }; + // Piscodemon + variants[44] = new List { "Z.", "Wz" }; + // Dragon 2 / Dragon Type 2 + variants[45] = new List { }; + // Knight Type 1 + variants[46] = new List { "Fr", "R.", "Z.", "Wz" }; + // Golem + variants[47] = new List { }; + // Knight Type 2 / Badman + variants[48] = new List { "Fr", "R.", "Z.", "Wz" }; + // Pony + variants[49] = new List { "Fr", "R.", "Z.", "Wr", "Sea" }; + // Elf + variants[50] = new List { "Fr", "R.", "Z.", "Sea" }; + // War Machine / Mech + variants[51] = new List { }; + + return variants; + } + + private List[] InitializeMonsterNameVariants() + { + var nameVariants = new List[52]; + for (int i = 0; i < 52; ++i) + { + nameVariants[i] = new List { "A.", "B.", "C.", "D.", "E.", "F.", "G.", "H.", "I.", "K.", "L.", "M.", "N.", "P.", "S.", "T.", "V.", "W.", "X." }; + } + return nameVariants; + } + + private bool[] InitializeMonsterBaseNameUsed() + { + var baseNameUsed = new bool[52]; + // if enemy is not part of a variant class, by default it uses the base name for the monster + for (int i = 0; i < 52; ++i) + { + baseNameUsed[i] = false; + } + + baseNameUsed[25] = true; // large elemental + baseNameUsed[27] = true; // dragon 1 + baseNameUsed[42] = true; // small elemental + baseNameUsed[45] = true; // dragon 2 + + return baseNameUsed; + } - [Description("Imp")] - Imp, - [Description("Imp (inc. Fiends and Chaos)")] - ImpAll } } diff --git a/FF1Lib/EnemizerVariant.cs b/FF1Lib/EnemizerVariant.cs new file mode 100644 index 000000000..45d931b89 --- /dev/null +++ b/FF1Lib/EnemizerVariant.cs @@ -0,0 +1,25 @@ +namespace FF1Lib +{ + public class EnemizerVariant + { + public string Name { get; set; } + public byte MonsterType { get; set; } + public byte ElemResist { get; set; } + public byte ElemWeakness { get; set; } + public (int min, int max)? CritRateRange { get; set; } + public bool IsLargeElemental { get; set; } + public bool IsSmallElemental { get; set; } + public bool IsDragon { get; set; } + public bool SpecialAbsorbChance { get; set; } + public bool SpecialGolemLogic { get; set; } + public bool SpecialFlanLogic { get; set; } + + public EnemizerVariant(string name, byte monsterType, byte elemResist, byte elemWeakness) + { + Name = name; + MonsterType = monsterType; + ElemResist = elemResist; + ElemWeakness = elemWeakness; + } + } +}