/// <summary>Calculates the final number of objects to spawn today in the current spawning process, based on config settings and player levels in a relevant skill.</summary> /// <param name="min">Minimum number of objects to spawn today (before skill multiplier).</param> /// <param name="max">Maximum number of objects to spawn today (before skill multiplier).</param> /// <param name="percent">Additive multiplier for each of the player's levels in the relevant skill (e.g. 10 would represent +10% objects per level).</param> /// <param name="skill">Enumerator for the skill on which the "percent" additive multiplier is based.</param> /// <returns>The final number of objects to spawn today in the current spawning process.</returns> public static int AdjustedSpawnCount(int min, int max, int percent, Utility.Skills skill) { int spawnCount = RNG.Next(min, max + 1); //random number from min to max (higher number is exclusive, so +1 to adjust for it) //calculate skill multiplier bonus double skillMultiplier = percent; skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. default config is "10" (10% per level) so it converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. with default config: "1.0" at level 0, "1.7" at level 7, etc //calculate final forage amount skillMultiplier *= spawnCount; //multiply the initial random spawn count by the skill multiplier spawnCount = (int)skillMultiplier; //store the integer portion of the current multiplied value (e.g. this is "1" if the multiplier is "1.7") double remainder = skillMultiplier - (int)skillMultiplier; //store the decimal portion of the multiplied value (e.g. this is "0.7" if the multiplier is "1.7") if (RNG.NextDouble() < remainder) //use remainder as a % chance to spawn one extra object (e.g. if the final count would be "1.7", there's a 70% chance of spawning 2 objects) { spawnCount++; } return(spawnCount); }
/// <summary>Produces a dictionary containing the final, adjusted spawn chance of each object in the provided dictionaries. (Part of the convoluted object spawning process for ore.)</summary> /// <param name="skill">The player skill that affects spawn chances (e.g. Mining for ore spawn chances).</param> /// <param name="levelRequired">A dictionary of object names and the skill level required to spawn them.</param> /// <param name="startChances">A dictionary of object names and their weighted chances to spawn at their lowest required skill level (e.g. chance of spawning stone if you're level 0).</param> /// <param name="maxChances">A dictionary of object names and their weighted chances to spawn at skill level 10.</param> /// <returns></returns> public static Dictionary <string, int> AdjustedSpawnChances(Utility.Skills skill, Dictionary <string, int> levelRequired, Dictionary <string, int> startChances, Dictionary <string, int> maxChances) { Dictionary <string, int> adjustedChances = new Dictionary <string, int>(); int skillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { skillLevel = Math.Max(skillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } foreach (KeyValuePair <string, int> objType in levelRequired) { int chance = 0; //chance of spawning this object if (objType.Value > skillLevel) { //skill is too low to spawn this object; leave it at 0% } else if (skillLevel >= 10) { chance = maxChances[objType.Key]; //level 10 skill; use the max level chance } else if (objType.Value == skillLevel) { chance = startChances[objType.Key]; //skill is the minimum required; use the starting chance } else //skill is somewhere in between "starting" and "level 10", so do math to set the chance somewhere in between them (i forgot the term for this kind of averaging, sry) { int count = 0; long chanceMath = 0; //used in case the chances are very large numbers for some reason for (int x = objType.Value; x < 10; x++) //loop from [minimum skill level for this object] to [max level - 1], for vague math reasons { if (skillLevel > x) { chanceMath += maxChances[objType.Key]; //add level 10 chance } else { chanceMath += startChances[objType.Key]; //add starting chance } count++; } chanceMath = (long)Math.Round((double)chanceMath / (double)count); //divide to get the average chance = Convert.ToInt32(chanceMath); //convert back to a reasonable number range once the math is done } if (chance > 0) //don't bother adding any objects with 0% or negative spawn chance { adjustedChances.Add(objType.Key, chance); //add the object name & chance to the list of adjusted chances } } return(adjustedChances); }
/// <summary>Apply any provided non-type-specific settings to a monster.</summary> /// <param name="monster">The monster to be customized. This will be modified by reference.</param> /// <param name="settings">A dictionary of setting names and values. If null, this method will do nothing.</param> /// <param name="areaID">The UniqueAreaID of the related SpawnArea. Required for log messages.</param> public static void ApplyMonsterSettings(Monster monster, Dictionary <string, object> settings, string areaID = "") { if (settings == null) //if no settings were provided { return; //do nothing } //set max HP if (settings.ContainsKey("HP")) { monster.MaxHealth = Convert.ToInt32(settings["HP"]); } //set damage if (settings.ContainsKey("Damage")) { monster.DamageToFarmer = Convert.ToInt32(settings["Damage"]); //set DamageToFarmer if (monster is ICustomDamage cd) //if this monster type uses the custom damage interface { cd.CustomDamage = Convert.ToInt32(settings["Damage"]); //set CustomDamage as well } } //set defense if (settings.ContainsKey("Defense")) { monster.resilience.Value = Convert.ToInt32(settings["Defense"]); } //set dodge chance if (settings.ContainsKey("DodgeChance")) { monster.missChance.Value = ((double)Convert.ToInt32(settings["DodgeChance"])) / 100; } //set experience points if (settings.ContainsKey("EXP")) { monster.ExperienceGained = Convert.ToInt32(settings["EXP"]); } //multiply HP and/or damage based on players' highest level in a skill if (settings.ContainsKey("RelatedSkill")) { //parse the provided skill into an enum Utility.Skills skill = (Utility.Skills)Enum.Parse(typeof(Utility.Skills), ((string)settings["RelatedSkill"]).Trim(), true); //parse while trimming whitespace and ignoring case //multiply HP if (settings.ContainsKey("PercentExtraHPPerSkillLevel")) { //calculate HP multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraHPPerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's max HP skillMultiplier *= monster.MaxHealth; //multiply the current max HP by the skill multiplier monster.MaxHealth = Math.Max((int)skillMultiplier, 1); //set the monster's new max HP (rounded down to the nearest integer & minimum 1) } //multiply damage if (settings.ContainsKey("PercentExtraDamagePerSkillLevel")) { //calculate damage multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraDamagePerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's damage skillMultiplier *= monster.DamageToFarmer; //multiply the current damage by the skill multiplier monster.DamageToFarmer = Math.Max((int)skillMultiplier, 0); //set the monster's new damage (rounded down to the nearest integer & minimum 0) } //multiply defense if (settings.ContainsKey("PercentExtraDefensePerSkillLevel")) { //calculate defense multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraDefensePerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's defense skillMultiplier *= monster.resilience.Value; //multiply the current damage by the skill multiplier monster.resilience.Value = Math.Max((int)skillMultiplier, 0); //set the monster's new defense (rounded down to the nearest integer & minimum 0) } //multiply dodge chance if (settings.ContainsKey("PercentExtraDodgeChancePerSkillLevel")) { //calculate dodge chance multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraDodgeChancePerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's dodge chance skillMultiplier *= monster.missChance.Value; //multiply the current damage by the skill multiplier monster.missChance.Value = Math.Max((int)skillMultiplier, 0); //set the monster's new dodge chance (rounded down to the nearest integer & minimum 0) } //multiply experience points if (settings.ContainsKey("PercentExtraEXPPerSkillLevel")) { //calculate exp multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraEXPPerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's exp skillMultiplier *= monster.ExperienceGained; //multiply the current exp by the skill multiplier monster.ExperienceGained = Math.Max((int)skillMultiplier, 0); //set the monster's new exp (rounded down to the nearest integer & minimum 0) } } //set loot (i.e. items dropped on death by the monster) if (settings.ContainsKey("Loot")) { List <SavedObject> loot = ((JArray)settings["Loot"]).ToObject <List <SavedObject> >(); //cast this list of saved objects (already validated and parsed elsewhere) MonsterTracker.SetLoot(monster, loot); //set the monster's loot in the monster tracker monster.objectsToDrop.Clear(); //clear any "default" loot the monster might've had } //set current HP //NOTE: do this after any max HP changes, because it may be contingent on max HP) if (settings.ContainsKey("CurrentHP")) { monster.Health = Convert.ToInt32(settings["CurrentHP"]); } else //if no current HP setting was provided { monster.Health = monster.MaxHealth; //set it to max HP } //set whether the monster automatically sees players at spawn if (settings.ContainsKey("SeesPlayersAtSpawn")) { if ((bool)settings["SeesPlayersAtSpawn"]) //if the setting is true (note: supposedly, some monsters behave incorrectly if focusedOnFarmers is set to false) { monster.focusedOnFarmers = true; //set the monster focus } } //set sprite if (settings.ContainsKey("Sprite")) { //replace the monster's sprite, using its existing settings where possible monster.Sprite = new AnimatedSprite((string)settings["Sprite"], monster.Sprite.CurrentFrame, monster.Sprite.SpriteWidth, monster.Sprite.SpriteHeight); } }
/// <summary>Apply any provided non-type-specific settings to a monster.</summary> /// <param name="monster">The monster to be customized. This will be modified by reference.</param> /// <param name="settings">A dictionary of setting names and values. If null, this method will do nothing.</param> /// <param name="areaID">The UniqueAreaID of the related SpawnArea. Required for log messages.</param> public static void ApplyMonsterSettings(Monster monster, Dictionary <string, object> settings, string areaID = "") { if (settings == null) //if no settings were provided { return; //do nothing } //set max HP if (settings.ContainsKey("HP")) { monster.MaxHealth = Convert.ToInt32(settings["HP"]); } //set damage if (settings.ContainsKey("Damage")) { monster.DamageToFarmer = Convert.ToInt32(settings["Damage"]); //set DamageToFarmer if (monster is ICustomDamage cd) //if this monster type uses the custom damage interface { cd.CustomDamage = Convert.ToInt32(settings["Damage"]); //set CustomDamage as well } } //set defense if (settings.ContainsKey("Defense")) { monster.resilience.Value = Convert.ToInt32(settings["Defense"]); } //set dodge chance if (settings.ContainsKey("DodgeChance")) { monster.missChance.Value = ((double)Convert.ToInt32(settings["DodgeChance"])) / 100; } //set experience points if (settings.ContainsKey("EXP")) { monster.ExperienceGained = Convert.ToInt32(settings["EXP"]); } //multiply HP and/or damage based on players' highest level in a skill if (settings.ContainsKey("RelatedSkill")) { //parse the provided skill into an enum Utility.Skills skill = (Utility.Skills)Enum.Parse(typeof(Utility.Skills), ((string)settings["RelatedSkill"]).Trim(), true); //parse while trimming whitespace and ignoring case //multiply HP if (settings.ContainsKey("PercentExtraHPPerSkillLevel")) { //calculate HP multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraHPPerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's max HP skillMultiplier *= monster.MaxHealth; //multiply the current max HP by the skill multiplier monster.MaxHealth = Math.Max((int)skillMultiplier, 1); //set the monster's new max HP (rounded down to the nearest integer & minimum 1) } //multiply damage if (settings.ContainsKey("PercentExtraDamagePerSkillLevel")) { //calculate damage multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraDamagePerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's damage skillMultiplier *= monster.DamageToFarmer; //multiply the current damage by the skill multiplier monster.DamageToFarmer = Math.Max((int)skillMultiplier, 0); //set the monster's new damage (rounded down to the nearest integer & minimum 0) } //multiply defense if (settings.ContainsKey("PercentExtraDefensePerSkillLevel")) { //calculate defense multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraDefensePerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's defense skillMultiplier *= monster.resilience.Value; //multiply the current damage by the skill multiplier monster.resilience.Value = Math.Max((int)skillMultiplier, 0); //set the monster's new defense (rounded down to the nearest integer & minimum 0) } //multiply dodge chance if (settings.ContainsKey("PercentExtraDodgeChancePerSkillLevel")) { //calculate dodge chance multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraDodgeChancePerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's dodge chance skillMultiplier *= monster.missChance.Value; //multiply the current damage by the skill multiplier monster.missChance.Value = Math.Max((int)skillMultiplier, 0); //set the monster's new dodge chance (rounded down to the nearest integer & minimum 0) } //multiply experience points if (settings.ContainsKey("PercentExtraEXPPerSkillLevel")) { //calculate exp multiplier based on skill level double skillMultiplier = Convert.ToInt32(settings["PercentExtraEXPPerSkillLevel"]); skillMultiplier = (skillMultiplier / 100); //converted to percent, e.g. "10" (10% per level) converts to "0.1" int highestSkillLevel = 0; //highest skill level among all existing farmers, not just the host foreach (Farmer farmer in Game1.getAllFarmers()) { highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the new level if it's higher than before } skillMultiplier = 1.0 + (skillMultiplier * highestSkillLevel); //final multiplier; e.g. if the setting is "10", this is "1.0" at level 0, "1.7" at level 7, etc //apply the multiplier to the monster's exp skillMultiplier *= monster.ExperienceGained; //multiply the current exp by the skill multiplier monster.ExperienceGained = Math.Max((int)skillMultiplier, 0); //set the monster's new exp (rounded down to the nearest integer & minimum 0) } } //set loot (i.e. items dropped on death by the monster) if (settings.ContainsKey("Loot")) { List <SavedObject> loot = ((JArray)settings["Loot"]).ToObject <List <SavedObject> >(); //cast this list of saved objects (already validated and parsed elsewhere) MonsterTracker.SetLoot(monster, loot); //set the monster's loot in the monster tracker monster.objectsToDrop.Clear(); //clear any "default" loot the monster might've had } //set current HP //NOTE: do this after any max HP changes, because it may be contingent on max HP) if (settings.ContainsKey("CurrentHP")) { monster.Health = Convert.ToInt32(settings["CurrentHP"]); } else //if no current HP setting was provided { monster.Health = monster.MaxHealth; //set it to max HP } //set whether the monster automatically sees players at spawn if (settings.ContainsKey("SeesPlayersAtSpawn")) { if ((bool)settings["SeesPlayersAtSpawn"]) //if the setting is true (note: supposedly, some monsters behave incorrectly if focusedOnFarmers is set to false) { monster.focusedOnFarmers = true; //set the monster focus } } //set sprite if (settings.ContainsKey("Sprite")) { //replace the monster's sprite, using its existing settings where possible monster.Sprite = new AnimatedSprite((string)settings["Sprite"], monster.Sprite.CurrentFrame, monster.Sprite.SpriteWidth, monster.Sprite.SpriteHeight); } //set facing direction if (settings.ContainsKey("FacingDirection")) { int facingDirection = 2; string directionString = (string)settings["FacingDirection"]; //get the provided setting switch (directionString.Trim().ToLower()) { //get an integer representing the provided direction case "up": facingDirection = 0; break; case "right": facingDirection = 1; break; case "down": facingDirection = 2; break; case "left": facingDirection = 3; break; } monster.faceDirection(facingDirection); //set monster direction } //set segments if (settings.ContainsKey("Segments")) { int segments = Convert.ToInt32(settings["Segments"]); if (monster is GreenSlime slime) //if this monster is a type of slime { segments = Math.Max(0, segments); //minimum = 0 slime.stackedSlimes.Value = segments; //set the number of additional slimes stacked on top of this slime } else if (monster is Serpent serpent) //if this monster is a type of serpent { if (segments < 2) { segments = 0; //minimum 0, but treat 1 as 0 (to avoid potential issues with royal/non-royal serpent distinctions) } serpent.segmentCount.Value = segments; //set the number of additional body segments this serpent has } } //set sight range if (settings.ContainsKey("SightRange")) { monster.moveTowardPlayer(Convert.ToInt32(settings["SightRange"])); //set "moveTowardPlayerThreshold" value and "isWalkingTowardPlayer" flag } //set extra loot if (settings.ContainsKey("ExtraLoot")) { if (settings["ExtraLoot"] is bool extraLoot && extraLoot == false) //if this setting is false { monster.modData[HarmonyPatch_ToggleExtraLoot.ModDataKey] = "false"; //flag this in the monster's mod data } } }