About this mod
Configurable probability and uniqueness multipliers for most Museum Artifact drops. And Elite Enemy / Rusty Key spawn multiplier.
- Requirements
- Permissions and credits
- Changelogs
- Donations
Adds configurable (F1) chance (flat modifier & skill based) and uniqueness (don't drop duplicates) modifiers to the following artifact sources:
- Mining.
- Combat.
- Exploration (woodcutting).
- Fishing (with a rod).
Also the following tweaks:
- Rusty Key chance modifiers.
- Elite Enemy chance modifiers.
- Fixes a bug that causes 'Book 1' of each town drop 66% less than the other 5. No matter what settings.
- If Museum Tracker mod is present, uniqueness checks your inventory and item description, instead of encyclopaedia to prevent issues if you lose an artifact.
The mod does not affect Golden animal/tree drops, because it's easy enough to control those with skills & more animals / fruit trees.
It also doesn't affect museum fish spawn rates. 'Fish Tweaks' makes finding the right fish easier, and 'No Time For Fishing' provides a multiplier for those.
This is something I threw together quickly for my own playthrough, I'm unlikely to develop it further / maintain it.
Installation:
- Download and Install the latest version of BepInEx.
- [Optional] Download and Install the latest version of Museum Tracker if you don't want the uniqueness modifier to apply if you lose an item.
- Download this mod from Files, and unzip the mod .dll into your BepInEx\plugins folder.
Compatibility:
- Should be compatible with most mods that affect same areas.
- Unless they edit the same exact place, in which case turning off certain features can help.
- Combat Artifact multiplier applied on a save will (probably) not reset if you remove the mod.
Source:
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using PSS;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using UnityEngine;
using Wish;
[BepInPlugin(MOD_GUID, MOD_NAME, MOD_VER)]
[BepInDependency(MOD_MUSEUM_TRACKER_GUID, BepInDependency.DependencyFlags.SoftDependency)]
public class ArtifactTweaksPlugin : BaseUnityPlugin
{
private const string MOD_GUID = "barteke22.sunhaven.artifact_tweaks";
private const string MOD_VER = "1.0.0";
private const string MOD_NAME = "Artifact Tweaks";
private const string MOD_MUSEUM_TRACKER_GUID = "MuseumTracker";
private Harmony _harmony = new Harmony(MOD_GUID);
public static ManualLogSource logger;
private static ConfigEntry<bool> conf_mine_book_skill;
private static ConfigEntry<float> conf_mine_book_mult;
private static ConfigEntry<bool> conf_mine_book_unique;
private static ConfigEntry<bool> conf_mine_key_skill;
private static ConfigEntry<float> conf_mine_key_mult;
private static ConfigEntry<bool> conf_combat_skill;
private static ConfigEntry<float> conf_combat_mult;
private static ConfigEntry<bool> conf_combat_unique;
private static ConfigEntry<float> conf_combat_elite_mult;
private static ConfigEntry<bool> conf_combat_elite_skill;
private static ConfigEntry<bool> conf_fish_skill;
private static ConfigEntry<float> conf_fish_mult;
private static ConfigEntry<bool> conf_fish_unique;
private static ConfigEntry<bool> conf_tree_skill;
private static ConfigEntry<float> conf_tree_mult;
private static ConfigEntry<bool> conf_tree_unique;
private static ConfigEntry<string> conf_data;
private static bool conf_museum_tracker = false;
private static List<int> explorationMuseumItems;
private static List<int> fishingMuseumItems;
private void Awake()
{
logger = Logger;
try
{
foreach (var plugin in Chainloader.PluginInfos)
{
var metadata = plugin.Value.Metadata;
if (metadata.GUID.Equals(MOD_MUSEUM_TRACKER_GUID))
{
conf_museum_tracker = true;
break;
}
}
var warning = Environment.NewLine + Environment.NewLine + (conf_museum_tracker ? "Museum Tracker mod found!" : "Museum Tracker mod missing!") + Environment.NewLine
+ " - If found: Drops when item not in Inventory and contains (0/1) in its description." + Environment.NewLine
+ " - If missing: Drops when item not in Encyclopaedia (never acquired). Means item won't drop again if lost somehow.";
conf_mine_book_skill = Config.Bind("Mining", "Books - Skill bonus", true, "Chance of getting a book rises with your mining level.");
conf_mine_book_mult = Config.Bind("Mining", "Books - Multiplier", 1f, "Multiplier applied to total book chance.");
conf_mine_book_unique = Config.Bind("Mining", "Books - Always unique", true, "Drop only unique books." + warning);
conf_mine_key_skill = Config.Bind("Mining", "Keys - Skill bonus", false, "Chance of Rusty Keys from mining raises with mining level.");
conf_mine_key_mult = Config.Bind("Mining", "Keys - Multiplier", 1f, "Multiplier applied to total Rusty Key chance from mining.");
conf_combat_skill = Config.Bind("Combat", "Artifact - Skill bonus", true, "Chance of getting an artifact rises with your combat level.");
conf_combat_mult = Config.Bind("Combat", "Artifact - Multiplier", 1f, "Multiplier applied to total artifact chance.");
conf_combat_unique = Config.Bind("Combat", "Artifact - Always unique", true, "Drop only unique artifacts." + warning);
conf_combat_elite_mult = Config.Bind("Combat", "Elite spawn - Multiplier", 1f, "Multiplier for Elite enemy spawn rate.");
conf_combat_elite_skill = Config.Bind("Combat", "Elite spawn - Skill bonus", true, "Elite enemy chance rises with your combat level.");
conf_fish_skill = Config.Bind("Fishing", "Artifact - Skill bonus", true, "Chance of getting an artifact rises with your fishing level. Fishing rod only.");
conf_fish_mult = Config.Bind("Fishing", "Artifact - Multiplier", 1f, "Multiplier applied to total artifact chance. Fishing rod only.");
conf_fish_unique = Config.Bind("Fishing", "Artifact - Always unique", true, "Drop only unique artifacts. Fishing rod only." + warning);
conf_tree_skill = Config.Bind("Exploration", "Skill bonus", true, "Chance of getting an artifact rises with your exploration level.");
conf_tree_mult = Config.Bind("Exploration", "Multiplier", 1f, "Multiplier applied to total artifact chance.");
conf_tree_unique = Config.Bind("Exploration", "Always unique", true, "Drop only unique artifacts." + warning);
Config.Bind("<Other>", "Golden Products", "<-- READ", "This mod doesn't affect golden animal/tree drops." + Environment.NewLine
+ "You can affect them yourself with the appropriate skills and by getting more animals / fruit trees.");
conf_data = Config.Bind("DATA", "DATA", "", new ConfigDescription("Do NOT EDIT or RESET this.", null, "Advanced"));
fishingMuseumItems = new List<int>(FishingRod.fishingMuseumItems);//hack: copy and empty artifact lists to avoid fully overwriting their methods
explorationMuseumItems = new List<int>(Wish.Tree.explorationMuseumItems);
typeof(FishingRod).GetField("fishingMuseumItems", BindingFlags.Public | BindingFlags.Static).SetValue(null, new List<int>() { 0 });
typeof(Wish.Tree).GetField("explorationMuseumItems", BindingFlags.Public | BindingFlags.Static).SetValue(null, new List<int>() { 0 });
_harmony.PatchAll();
logger.LogInfo(MOD_GUID + " v" + MOD_VER + " loaded.");
}
catch (Exception e)
{
logger.LogError("** Awake FATAL - " + e);
}
}
public static void LogError(Exception e)
{
logger.LogError(e.Message + Environment.NewLine + e.StackTrace);
}
//mining
[HarmonyPatch(typeof(MineGenerator2), "AttemptToDropRustyKey")]//rusty key multiplier
private class HarmonyPatch_MineGenerator2_AttemptToDropRustyKey
{
private static bool Prefix(MineGenerator2 __instance, Vector3 position, float multiplier = 1f)
{
try
{
if (__instance.canDropRustyKey)
{
if (conf_mine_key_skill.Value)
{
multiplier += (0.005f * SingletonBehaviour<GameSave>.Instance.CurrentSave.characterData.Professions[ProfessionType.Mining].level) - 0.005f;
}
multiplier *= conf_mine_key_mult.Value;
var rustykeyAttempts = typeof(MineGenerator2).GetField("rustykeyAttempts", BindingFlags.NonPublic | BindingFlags.Instance);
var rustykeyAttemptsVal = (float)rustykeyAttempts.GetValue(__instance);
if (SceneSettingsManager.Instance.sceneDictionary[ScenePortalManager.ActiveSceneIndex].mapType == MapType.Mine
&& !SingletonBehaviour<GameSave>.Instance.GetProgressBoolWorld("minesUnlock" + __instance.tier)
&& !SingletonBehaviour<GameSave>.Instance.GetProgressBoolCharacter("droppedRustyKey" + __instance.tier)
&& Wish.Utilities.Chance((0.04f + 0.0129999993f * rustykeyAttemptsVal) * multiplier * __instance.rustyKeyDropRateMultiplier * Mathf.Lerp(0.85f, 0.4f, (float)__instance.tier / 50f))
&& rustykeyAttemptsVal >= 10f)
{
__instance.SpawnRustyKey(position);
}
rustykeyAttempts.SetValue(__instance, rustykeyAttemptsVal + multiplier);
}
return false;
}
catch (Exception e)
{
LogError(e);
}
return true;
}
}
[HarmonyPatch(typeof(MineGenerator2), "AttemptToDropBook")]//book multiplier
private class HarmonyPatch_MineGenerator2_AttemptToDropBook
{
private static bool Prefix(MineGenerator2 __instance, Vector3 position)
{
try
{
var chance = 0.0025f;
if (conf_mine_book_skill.Value)
{
chance += (0.0005f * SingletonBehaviour<GameSave>.Instance.CurrentSave.characterData.Professions[ProfessionType.Mining].level) - 0.0005f;
}
chance *= conf_mine_book_mult.Value;
if (__instance.canDropBook && SceneSettingsManager.Instance.sceneDictionary[ScenePortalManager.ActiveSceneIndex].mapType == MapType.Mine && Wish.Utilities.Chance(chance))
{
__instance.SpawnBook(position);
}
return false;
}
catch (Exception e)
{
LogError(e);
}
return true;
}
}
[HarmonyPatch(typeof(MineGenerator2), "SpawnBook")]//book uniqueness
private class HarmonyPatch_MineGenerator2_SpawnBook
{
private static bool Prefix(MineGenerator2 __instance, Vector3 position)
{
try
{
var _bookDrop = (List<ItemData>)typeof(MineGenerator2).GetField("_bookDrop", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
TrySpawnMuseumItem(_bookDrop.Select(f => f.id).ToList(), conf_mine_book_unique.Value, position);
return false;
}
catch (Exception e)
{
LogError(e);
}
return true;
}
}
//item uniqueness
private static void TrySpawnMuseumItem(List<int> itemIDs, bool unique, Vector3 position, float homeInDelay = 0.4f)
{
var itemId = TryGetMuseumItem(itemIDs, unique);
if (itemId != 0)
{
Pickup.Spawn(position.x, position.y, position.z, itemId, homeInDelay: homeInDelay);
}
}
private static int TryGetMuseumItem(List<int> itemIDs, bool unique)
{
List<int> ids = new List<int>();
foreach (var id in itemIDs)
{
if (TryGetMuseumItem(id, unique)) ids.Add(id);
}
if (ids.Count > 0) return ids.RandomItem();
return 0;
}
private static bool TryGetMuseumItem(int id, bool unique)
{
if (unique)
{
if (conf_museum_tracker)//safer because checks museum tracker & inventory
{
if (!Player.Instance.PlayerInventory.HasEnough(id, 1))
{
ItemData data = null;
Database.GetData(id, delegate (ItemData d)
{
data = d;
});
if (data?.FormattedDescription.Contains("(0/1)</size>") == true)
{
return true;
}
}
}
else//less safe because player can have lost item
{
if (!GameSave.CurrentCharacter.Encylopdeia.ContainsKey((short)id)) return true;
}
}
else
{
if (!GameSave.CurrentCharacter.Encylopdeia.ContainsKey((short)id) || !Wish.Utilities.Chance(0.6666f))
{
return true;
}
}
return false;
}
//combat
[HarmonyPatch]//generic trinket multiplier & uniqueness
private class HarmonyPatch_EnemyAI_Die
{
[HarmonyReversePatch]
[HarmonyPatch(typeof(AI), "Die")]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void BaseDie(AI instance, bool fromLocalPlayer)
{
Console.WriteLine($"Patch.Die({instance}, {fromLocalPlayer})");
}
[HarmonyPatch(typeof(EnemyAI), "Die")]
private static bool Prefix(EnemyAI __instance, bool fromLocalPlayer)
{
try
{
if (!fromLocalPlayer || !__instance.DropItems || (!conf_combat_unique.Value && !conf_combat_skill.Value && conf_combat_mult.Value == 1f)) return true;//only local gets these
if ((bool)typeof(EnemyAI).GetField("_dead", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance))
{
return false;
}
HarmonyPatch_AI_Die.Prefix(__instance, fromLocalPlayer);
BaseDie(__instance, fromLocalPlayer);
if (fromLocalPlayer)
{
if (__instance.DropItems)
{
float num = (float)typeof(EnemyAI).GetField("_experience", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
if (GameSave.Combat.GetNode("Combat4c"))
{
num += (float)GameSave.Combat.GetNodeAmount("Combat4c");
}
Player.Instance.AddEXP(ProfessionType.Combat, num);
TownType currentTownType = SingletonBehaviour<DayCycle>.Instance.CurrentTownType;
var chance = (currentTownType == TownType.SunHaven) ? 0.00125f : 0.0025f;
if (conf_combat_skill.Value)
{
chance += (0.0003f * SingletonBehaviour<GameSave>.Instance.CurrentSave.characterData.Professions[ProfessionType.Combat].level) - 0.0003f;
}
chance *= conf_combat_mult.Value;
if (Wish.Utilities.Chance(chance))
{
int num2 = 20100;
switch (currentTownType)
{
case TownType.Withergate:
num2 = 20102;
break;
case TownType.Nelvari:
num2 = 20101;
break;
}
TrySpawnMuseumItem(new List<int> { num2 }, conf_combat_unique.Value, __instance.transform.position);
}
}
SingletonBehaviour<GameSave>.Instance.SetProgressFloatCharacter(__instance.enemyName + "killed", SingletonBehaviour<GameSave>.Instance.GetProgressFloatCharacter(__instance.enemyName + "killed") + 1f);
if (GameSave.Combat.GetNode("Combat3a"))
{
float a = 0.5f + 0.5f * (float)GameSave.Combat.GetNodeAmount("Combat3a");
Player.Instance.AddMana(Mathf.Max(a, Player.Instance.MaxMana * 0.01f));
}
}
__instance.onDie?.Invoke();
return false;
}
catch (Exception e)
{
LogError(e);
}
return true;
}
}
[HarmonyPatch(typeof(AI), "Die")]//monster-specific trinket multiplier & uniqueness
private class HarmonyPatch_AI_Die
{
public static bool Prefix(AI __instance, bool fromLocalPlayer)
{
try
{
if (!fromLocalPlayer || (!conf_combat_unique.Value && !conf_combat_skill.Value && conf_combat_mult.Value == 1f)) return true;//only local gets these
if ((bool)typeof(AI).GetField("_dead", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance))
{
return false;
}
var ogValues = conf_data.Value.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Split(':'))
.ToDictionary(x => int.Parse(x[0]), x => float.Parse(x[1]));
var _drops2 = (List<RandomArray2>)typeof(AI).GetField("_drops2", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
var any = false;
foreach (var drop in _drops2)
{
foreach (var item in drop.drops)
{
if ((uint)(item.id - 20103) <= 10u)
{
if (!ogValues.TryGetValue(item.id, out var chance))
{
chance = item.dropChance;
ogValues.Add(item.id, chance);
any = true;
}
if (TryGetMuseumItem(item.id, conf_combat_unique.Value))
{
if (conf_combat_skill.Value)
{
chance += (0.0003f * SingletonBehaviour<GameSave>.Instance.CurrentSave.characterData.Professions[ProfessionType.Combat].level) - 0.0003f;
}
chance *= conf_combat_mult.Value;
}
else chance = 0;
item.dropChance = chance;
}
}
}
if (any)
{
var data = "";
foreach (var item in ogValues)
{
data += item.Key + ":" + item.Value + ";";
}
conf_data.Value = data;
}
}
catch (Exception e)
{
LogError(e);
}
return true;
}
}
[HarmonyPatch(typeof(EnemySpawnGroup), "SpawnGroup")]//elite spawn multiplier
private class HarmonyPatch_EnemySpawnGroup_SpawnGroup
{
private static bool Prefix(EnemySpawnGroup __instance)
{
try
{
if (conf_combat_elite_skill.Value || conf_combat_elite_mult.Value != 1f)
{
typeof(EnemySpawnGroup).GetField("readyForSpawn", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, false);
typeof(EnemySpawnGroup).GetField("spawnTween", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, null);
var SpawnerHasEnemy = typeof(EnemySpawnGroup).GetMethod("SpawnerHasEnemy", BindingFlags.NonPublic | BindingFlags.Instance);
foreach (EnemySpawner spawner in __instance.spawners)
{
if ((bool)SpawnerHasEnemy.Invoke(__instance, new object[] { spawner }) || !spawner)
{
continue;
}
string text = spawner.enemy;
bool flag = false;
float chance = 0.04f;
if (conf_combat_elite_skill.Value)
{
chance += (0.001f * SingletonBehaviour<GameSave>.Instance.CurrentSave.characterData.Professions[ProfessionType.Combat].level) - 0.001f;
}
if (Wish.Utilities.Chance(chance * conf_combat_elite_mult.Value) && EnemyManager.Instance.enemiesDictionary.ContainsKey("Elite" + text))
{
text = "Elite" + text;
flag = true;
}
if (!Wish.Utilities.Chance(spawner.spawnChance))
{
continue;
}
if (!flag && spawner.hasSeasonalSprites)
{
switch (SingletonBehaviour<DayCycle>.Instance.Season)
{
case Season.Summer:
text += "_Summer";
break;
case Season.Fall:
text += "_Fall";
break;
case Season.Winter:
text += "_Winter";
break;
case Season.Spring:
text += "_Spring";
break;
}
}
EnemyManager.Instance.SpawnEnemy(text, spawner.transform.position, spawner.ID);
}
}
return false;
}
catch (Exception e)
{
LogError(e);
}
return true;
}
}
//Fishing
[HarmonyPatch(typeof(FishingRod), "CatchFishWithDialogue")]//fishing uniqueness
private class HarmonyPatch_FishingRod_CatchFishWithDialogue
{
private static void Postfix(FishingRod __instance)
{
try
{
if (__instance.Player == Player.Instance)
{
var chance = 0.05f;
if (conf_fish_skill.Value)
{
chance += (0.0005f * SingletonBehaviour<GameSave>.Instance.CurrentSave.characterData.Professions[ProfessionType.Fishing].level) - 0.0005f;
}
if (Wish.Utilities.Chance(chance * conf_fish_mult.Value))
{
var id = TryGetMuseumItem(fishingMuseumItems, conf_fish_unique.Value);
if (id != 0)
{
Player.Instance.Inventory.AddItem(id);
}
}
}
}
catch (Exception e)
{
LogError(e);
}
}
}
//Exploration
[HarmonyPatch(typeof(Wish.Tree), "SpawnDrops")]//fishing uniqueness
private class HarmonyPatch_Tree_SpawnDrops
{
private static void Postfix(Wish.Tree __instance, Vector3 fallPosition, bool stumpDrop)
{
try
{
if ((bool)typeof(Wish.Tree).GetProperty("FullyGrown", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance))
{
var chance = stumpDrop ? 0.02f : 0.035f;
if (conf_tree_skill.Value)
{
chance += (0.0005f * SingletonBehaviour<GameSave>.Instance.CurrentSave.characterData.Professions[ProfessionType.Exploration].level) - 0.0005f;
}
if (Wish.Utilities.Chance(chance * conf_tree_mult.Value))
{
TrySpawnMuseumItem(explorationMuseumItems, conf_tree_unique.Value, new Vector3(fallPosition.x, (fallPosition.y - 0.5f) * 1.41421354f, 0f));
}
}
}
catch (Exception e)
{
LogError(e);
}
}
}
}