From 35b7772d252f4e903b0edb669e0339a50d6b04ca Mon Sep 17 00:00:00 2001 From: hunter-chambers Date: Thu, 25 Apr 2024 17:32:57 -0500 Subject: [PATCH] DeluxeAutoPetter v1.0.0 --- DeluxeAutoPetter/DeluxeAutoPetter.cs | 154 +++++++++++++ DeluxeAutoPetter/DeluxeAutoPetter.csproj | 19 ++ DeluxeAutoPetter/DeluxeAutoPetter.sln | 25 +++ .../assets/TileSheets/DeluxeAutoPetter.xnb | Bin 0 -> 1875 bytes .../helpers/DeluxeAutoPetterDrawPatcher.cs | 65 ++++++ .../helpers/MultiplayerHandler.cs | 68 ++++++ DeluxeAutoPetter/helpers/ObjectDetails.cs | 47 ++++ DeluxeAutoPetter/helpers/QuestDetails.cs | 202 ++++++++++++++++++ DeluxeAutoPetter/helpers/QuestMail.cs | 62 ++++++ DeluxeAutoPetter/helpers/ShopDetails.cs | 29 +++ DeluxeAutoPetter/i18n/default.json | 14 ++ DeluxeAutoPetter/manifest.json | 10 + 12 files changed, 695 insertions(+) create mode 100644 DeluxeAutoPetter/DeluxeAutoPetter.cs create mode 100644 DeluxeAutoPetter/DeluxeAutoPetter.csproj create mode 100644 DeluxeAutoPetter/DeluxeAutoPetter.sln create mode 100644 DeluxeAutoPetter/assets/TileSheets/DeluxeAutoPetter.xnb create mode 100644 DeluxeAutoPetter/helpers/DeluxeAutoPetterDrawPatcher.cs create mode 100644 DeluxeAutoPetter/helpers/MultiplayerHandler.cs create mode 100644 DeluxeAutoPetter/helpers/ObjectDetails.cs create mode 100644 DeluxeAutoPetter/helpers/QuestDetails.cs create mode 100644 DeluxeAutoPetter/helpers/QuestMail.cs create mode 100644 DeluxeAutoPetter/helpers/ShopDetails.cs create mode 100644 DeluxeAutoPetter/i18n/default.json create mode 100644 DeluxeAutoPetter/manifest.json diff --git a/DeluxeAutoPetter/DeluxeAutoPetter.cs b/DeluxeAutoPetter/DeluxeAutoPetter.cs new file mode 100644 index 0000000..d270986 --- /dev/null +++ b/DeluxeAutoPetter/DeluxeAutoPetter.cs @@ -0,0 +1,154 @@ +using DeluxeAutoPetter.helpers; +using HarmonyLib; +using Microsoft.Xna.Framework; +using StardewModdingAPI; +using StardewModdingAPI.Events; +using StardewValley; + +namespace DeluxeAutoPetter +{ + internal sealed class DeluxeAutoPetter : Mod + { + private static bool IS_DATA_LOADED = false; + + public override void Entry(IModHelper helper) + { + I18n.Init(helper.Translation); + + helper.Events.Multiplayer.ModMessageReceived += OnModMessageReceived; + helper.Events.Multiplayer.PeerConnected += OnPeerConnected; + + helper.Events.GameLoop.GameLaunched += OnGameLaunched; + helper.Events.GameLoop.ReturnedToTitle += OnReturnedToTitle; + + helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + helper.Events.Input.ButtonPressed += OnButtonPressed; + helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; + helper.Events.Player.InventoryChanged += OnPlayerInventoryChanged; + helper.Events.GameLoop.DayStarted += OnDayStarted; + helper.Events.GameLoop.DayEnding += OnDayEnding; + } + + private void OnModMessageReceived(object? sender, ModMessageReceivedEventArgs e) + { + if (e.FromModID.Equals(ModManifest.UniqueID)) + { + if (e.Type.Equals(nameof(MultiplayerHandler.QuestData))) + { + if (Context.IsMainPlayer) + { + MultiplayerHandler.SetPlayerQuestData(e.FromPlayerID, e.ReadAs()); + MultiplayerHandler.SavePerPlayerQuestData(Helper); + } + else + { + MultiplayerHandler.SetPlayerQuestData(Game1.player.UniqueMultiplayerID, e.ReadAs()); + QuestDetails.LoadQuestData(Game1.player.UniqueMultiplayerID); + } + } + } + } + + private void OnPeerConnected(object? sender, PeerConnectedEventArgs e) + { + if (!Context.IsMainPlayer) return; + + Helper.Multiplayer.SendMessage(MultiplayerHandler.GetPlayerQuestData(e.Peer.PlayerID), nameof(MultiplayerHandler.QuestData), new[] { ModManifest.UniqueID }, new[] { e.Peer.PlayerID }); + } + + private void OnReturnedToTitle(object? sender, ReturnedToTitleEventArgs e) + { + IS_DATA_LOADED = false; + } + + private void OnGameLaunched(object? sender, GameLaunchedEventArgs e) + { + DeluxeAutoPetterDrawPatcher.Initialize(Monitor); + Harmony harmony = new(ModManifest.UniqueID); + DeluxeAutoPetterDrawPatcher.ApplyPatch(harmony); + } + + private void OnUpdateTicked(object? sender, UpdateTickedEventArgs e) + { + if (!Context.IsWorldReady) return; + + if (Game1.player.hasQuest(QuestDetails.GetQuestID())) + { + QuestDetails.ShowDropboxLocator(true); + if (Game1.activeClickableMenu is null && QuestDetails.IsMouseOverDropbox(Game1.currentCursorTile)) Game1.mouseCursor = Game1.cursor_grab; + } + } + + private void OnButtonPressed(object? sender, ButtonPressedEventArgs e) + { + if (!(Context.IsWorldReady && + Game1.currentLocation.Equals(Game1.getLocationFromName(QuestDetails.GetDropBoxGameLocationString())) && + Game1.player.hasQuest(QuestDetails.GetQuestID()) && + e.Button.IsActionButton())) + return; + + Vector2 distanceVector = QuestDetails.GetInteractionDistanceFromDropboxVector(Game1.player.GetToolLocation()); + + if (Math.Abs(distanceVector.X) < (Game1.tileSize * 1.5) && Math.Abs(distanceVector.Y) < Game1.tileSize / 2) + Game1.activeClickableMenu ??= QuestDetails.CreateQuestContainerMenu(); + } + + private void OnSaveLoaded(object? sender, SaveLoadedEventArgs e) + { + QuestDetails.Initialize(ModManifest.UniqueID); + QuestMail.Initialize(ModManifest.UniqueID); + ObjectDetails.Initialize(ModManifest.UniqueID, Helper.DirectoryPath); + ShopDetails.Initialize(); + MultiplayerHandler.Initialize(ModManifest.UniqueID); + + QuestDetails.LoadQuest(); + QuestMail.LoadMail(QuestMail.GetQuestMailID(), QuestMail.GetQuestMailDetails()); + QuestMail.LoadMail(QuestMail.GetQuestRewardMailID(), QuestMail.GetQuestRewardMailDetails()); + ObjectDetails.LoadObject(); + ShopDetails.LoadShopItem(); + + if (Context.IsMainPlayer) + { + MultiplayerHandler.LoadPerPlayerQuestData(Helper, Game1.player.UniqueMultiplayerID); + QuestDetails.LoadQuestData(Game1.player.UniqueMultiplayerID); + IS_DATA_LOADED = true; + } + else Monitor.Log("You are not the host. If the host does not have this mod, then all data for this mod will be ignored.", LogLevel.Info); + } + + private void OnPlayerInventoryChanged(object? sender, InventoryChangedEventArgs e) + { + if (QuestDetails.IsQuestDataNull() && !Context.IsMainPlayer) return; + + if (!QuestDetails.GetIsTriggered() && e.Added.Any(item => item.QualifiedItemId.Equals(QuestDetails.GetAutoPetterID()))) + { + e.Player.mailForTomorrow.Add(QuestMail.GetQuestMailID()); + QuestDetails.SetIsTriggered(true); + } + } + + private void OnDayStarted(object? sender, DayStartedEventArgs e) + { + if (!Context.IsMainPlayer) return; + + foreach (StardewValley.Buildings.Building building in Game1.getFarm().buildings) + { + if (building.GetIndoors()?.Objects.Values.FirstOrDefault(sObject => sObject?.QualifiedItemId.Equals($"(BC){ObjectDetails.GetDeluxeAutoPetterID()}") ?? false, null) is not null) + { + foreach (FarmAnimal animal in building.GetIndoors().Animals.Values) + { + animal.pet(Game1.getFarmer(animal.ownerID.Value), true); + animal.pet(Game1.getFarmer(animal.ownerID.Value)); + } + } + } + } + + private void OnDayEnding(object? sender, DayEndingEventArgs e) + { + if (!IS_DATA_LOADED && Context.IsMainPlayer) return; + else if (Context.IsMainPlayer) MultiplayerHandler.SavePerPlayerQuestData(Helper); + else Helper.Multiplayer.SendMessage(MultiplayerHandler.GetPlayerQuestData(Game1.player.UniqueMultiplayerID), nameof(MultiplayerHandler.QuestData), new[] { ModManifest.UniqueID }); + } + } +} diff --git a/DeluxeAutoPetter/DeluxeAutoPetter.csproj b/DeluxeAutoPetter/DeluxeAutoPetter.csproj new file mode 100644 index 0000000..5bd15a6 --- /dev/null +++ b/DeluxeAutoPetter/DeluxeAutoPetter.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + true + + + + + + + + + + + + diff --git a/DeluxeAutoPetter/DeluxeAutoPetter.sln b/DeluxeAutoPetter/DeluxeAutoPetter.sln new file mode 100644 index 0000000..d11cee3 --- /dev/null +++ b/DeluxeAutoPetter/DeluxeAutoPetter.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeluxeAutoPetter", "DeluxeAutoPetter.csproj", "{B84F31E5-4B70-4A26-99DC-3DBE25913667}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B84F31E5-4B70-4A26-99DC-3DBE25913667}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B84F31E5-4B70-4A26-99DC-3DBE25913667}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B84F31E5-4B70-4A26-99DC-3DBE25913667}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B84F31E5-4B70-4A26-99DC-3DBE25913667}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EA18B832-0783-4A98-8994-1E43C1DDC272} + EndGlobalSection +EndGlobal diff --git a/DeluxeAutoPetter/assets/TileSheets/DeluxeAutoPetter.xnb b/DeluxeAutoPetter/assets/TileSheets/DeluxeAutoPetter.xnb new file mode 100644 index 0000000000000000000000000000000000000000..4861aabbc8ca1cabb675f75760ff8a2686d2466a GIT binary patch literal 1875 zcmZux-)kII6#i!RPBW8CI-@q4kXm+Q5)xSBY_oxV*u}9KG}0JW-D>EIrr8Nu5_i+h zhD4~ag**gFK{SGe`s9NZ3MwcR*@u1TLm!Ix&=v!&K{DliU+1DC7kZI9QnarA4-I^0LbI5KZ{omEr0ENbD=MMzh&X13gi$nK{;D(;nH#bp ze8I>_q7Ljth-EyFJZzqX7~u+`(Dd>cv1c^LP?tq#Uxecs?VbwcqGB}DF{m3RNJT+&_^G4;xa+mf7)%EF$MoR+MJNsJDzmnJUt zK7DUJ7!rwoQK)E+mD6A{r7z4(Vpl z$C1MNAQVP}SaL{8UD%}5HjMko__GhsATq!b=(a54DZ)iiZG)VW5wPUI)h*+EQXoB6 z!%{904@FUfqbFS@Nab%3DanUTr5Mel5H1&onqtv#sglpgGIBo^P&H)ui*j=$_px@O zE7LJ@=@c|AowY=EA$0(m`WxcKnK&Dqo&LHl!xG-U0GXT!*w*vI3FTH3wps|B(V7p8 z>LY3#XkN)_h#^Pc&d`uMkpQQ-x?{3$cwh4`_ivX#q@iYD?SnpwTaD5mPMw2XP3v{B zQ+47HS7G1<3=Sd18%NAdUF#>hRt4qiL3Z(s1W>QZz6gn$B8Ya9*NmrnJ9tZ)km5t) zus2LXo_0&1gdrF7;sYmrB3*sQ1nrzoobMky$9Pr<9vN&v0wr}rzxFn&ok5tG4_ zz+>Gox^c~N|8mRz%gy36-hw%-H5lZBV-6xC-UD4I4otOAQV4Sb*-)ZWhpt?P2^lek z7l$6`C*vLzv)t2sSRB_JqK-T;DV0hZBE~?w(SeYsZcYfbBv*54k&_}X^Q!LXCZeC; zoRxt|rXsCj16U2@IVwknygV}+v#1sSk6Q@OK`uqSF9c5}lsX$qUYNw{6=@=k#7ci3 Z3j+^f+|^frZ;$hj9mVp_ 0) ? Game1.random.Next(-1, 2) : 0), (int)(vector2.Y - vector.Y / 2f) + ((__instance.shakeTimer > 0) ? Game1.random.Next(-1, 2) : 0), (int)(64f + vector.X), (int)(128f + vector.Y / 2f)); + float num = Math.Max(0f, (float)((y + 1) * 64 - 24) / 10000f) + (float)x * 1E-05f; + + spriteBatch.Draw(texture, destinationRectangle, dataOrErrorItem.GetSourceRect(1, __instance.ParentSheetIndex), Color.White * alpha, 0f, Vector2.Zero, SpriteEffects.None, num); + spriteBatch.Draw(texture, vector2 + new Vector2(8.5f, 12f) * 4f, dataOrErrorItem.GetSourceRect(2, __instance.ParentSheetIndex), Color.White * alpha, (float)Game1.currentGameTime.TotalGameTime.TotalSeconds * -1.5f, new Vector2(7.5f, 15.5f), 4f, SpriteEffects.None, num + 1E-05f); + // this is the second set of rotating hands + spriteBatch.Draw(texture, vector2 + new Vector2(8.5f, 12f) * 4f, dataOrErrorItem.GetSourceRect(2, __instance.ParentSheetIndex), Color.White * alpha, (float)Game1.currentGameTime.TotalGameTime.TotalSeconds * -1.5f - 1.5f, new Vector2(7.5f, 15.5f), 4f, SpriteEffects.None, num + 1E-05f); + + return false; + } + + return true; + } + catch (Exception ex) + { + MONITOR?.Log($"Failed in {nameof(Draw_Prefix)}:\n{ex.Message}", LogLevel.Error); + return true; + } + } + } +} diff --git a/DeluxeAutoPetter/helpers/MultiplayerHandler.cs b/DeluxeAutoPetter/helpers/MultiplayerHandler.cs new file mode 100644 index 0000000..362075e --- /dev/null +++ b/DeluxeAutoPetter/helpers/MultiplayerHandler.cs @@ -0,0 +1,68 @@ +using StardewModdingAPI; + +namespace DeluxeAutoPetter.helpers +{ + internal static class MultiplayerHandler + { + public class QuestData + { + public bool IsTriggered { get; set; } = false; + public Dictionary DonationCounts { get; set; } = new() { + { QuestDetails.GetAutoPetterID(), 0 }, + { QuestDetails.GetHardwoodID(), 0 }, + { QuestDetails.GetIridiumBarID(), 0 } + }; + } + + private static string? PER_PLAYER_QUEST_DATA_KEY; + private static Dictionary? PER_PLAYER_QUEST_DATA; + + public static void Initialize(string modID) + { + PER_PLAYER_QUEST_DATA_KEY = $"{modID}.Quest.PER_PLAYER_QUEST_DATA_KEY"; + } + + public static void LoadPerPlayerQuestData(IModHelper helper, long hostID) + { + if (PER_PLAYER_QUEST_DATA_KEY is null) throw new ArgumentNullException($"{nameof(PER_PLAYER_QUEST_DATA_KEY)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + PER_PLAYER_QUEST_DATA = helper.Data.ReadSaveData>(PER_PLAYER_QUEST_DATA_KEY); + PER_PLAYER_QUEST_DATA ??= new() { { hostID, new() } }; + } + + public static void SavePerPlayerQuestData(IModHelper helper) + { + if (PER_PLAYER_QUEST_DATA_KEY is null) throw new ArgumentNullException($"{nameof(PER_PLAYER_QUEST_DATA_KEY)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + if (PER_PLAYER_QUEST_DATA is null) throw new ArgumentNullException($"{nameof(PER_PLAYER_QUEST_DATA)} has not been initialized! The {nameof(LoadPerPlayerQuestData)} method must be called first!"); + + helper.Data.WriteSaveData(PER_PLAYER_QUEST_DATA_KEY, PER_PLAYER_QUEST_DATA); + } + + public static QuestData GetPlayerQuestData(long playerID) + { + if (PER_PLAYER_QUEST_DATA is null) throw new ArgumentNullException($"{nameof(PER_PLAYER_QUEST_DATA)} has not been initialized! The {nameof(LoadPerPlayerQuestData)} method must be called first!"); + if (!PER_PLAYER_QUEST_DATA.ContainsKey(playerID) && !Context.IsMainPlayer) throw new ArgumentNullException($"Your {nameof(PER_PLAYER_QUEST_DATA)} does not have any quest data for your player ID!"); + + if (!PER_PLAYER_QUEST_DATA.ContainsKey(playerID) && Context.IsMainPlayer) CreatePlayerQuestData(playerID); + + return PER_PLAYER_QUEST_DATA[playerID]; + } + + public static void SetPlayerQuestData(long playerID, QuestData questData) + { + if (PER_PLAYER_QUEST_DATA is null && Context.IsMainPlayer) throw new ArgumentNullException($"{nameof(PER_PLAYER_QUEST_DATA)} has not been initialized! The {nameof(LoadPerPlayerQuestData)} method must be called first!"); + else if (PER_PLAYER_QUEST_DATA is null) PER_PLAYER_QUEST_DATA = new() { { playerID, questData } }; + + if (!PER_PLAYER_QUEST_DATA.ContainsKey(playerID)) throw new ArgumentNullException($"{nameof(PER_PLAYER_QUEST_DATA)} has no data associated with the playerID '{playerID}'."); + + PER_PLAYER_QUEST_DATA[playerID] = questData; + } + + private static void CreatePlayerQuestData(long playerID) + { + if (PER_PLAYER_QUEST_DATA is null) throw new ArgumentNullException($"{nameof(PER_PLAYER_QUEST_DATA)} has not been initialized! The {nameof(LoadPerPlayerQuestData)} method must be called first!"); + + PER_PLAYER_QUEST_DATA.Add(playerID, new()); + } + } +} diff --git a/DeluxeAutoPetter/helpers/ObjectDetails.cs b/DeluxeAutoPetter/helpers/ObjectDetails.cs new file mode 100644 index 0000000..2773086 --- /dev/null +++ b/DeluxeAutoPetter/helpers/ObjectDetails.cs @@ -0,0 +1,47 @@ +using StardewValley; +using StardewValley.GameData.BigCraftables; + +namespace DeluxeAutoPetter.helpers +{ + internal static class ObjectDetails + { + /** ********************** + * Class Variables + ********************** **/ + private static string? DELUXE_AUTO_PETTER_ID; + private static BigCraftableData? DELUXE_AUTO_PETTER_DETAILS; + + /** ********************** + * Variable Getters + ********************** **/ + public static string GetDeluxeAutoPetterID() + { + if (DELUXE_AUTO_PETTER_ID is null) throw new ArgumentNullException($"{nameof(DELUXE_AUTO_PETTER_ID)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + return DELUXE_AUTO_PETTER_ID; + } + + + /** ********************** + * Public Methods + ********************** **/ + public static void Initialize(string modID, string modDirectoryPath) + { + DELUXE_AUTO_PETTER_ID = $"{modID}.DeluxeAutoPetterObject"; + DELUXE_AUTO_PETTER_DETAILS = new() + { + Name = "Deluxe Auto-Petter", + DisplayName = I18n.DeluxeAutoPetterDisplayName(), + Description = I18n.DeluxeAutoPetterDescription(), + Texture = Path.Combine(modDirectoryPath, "assets", "TileSheets", "DeluxeAutoPetter.xnb") + }; + } + + public static void LoadObject() + { + if (DELUXE_AUTO_PETTER_DETAILS is null) throw new ArgumentNullException($"{nameof(DELUXE_AUTO_PETTER_DETAILS)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + DataLoader.BigCraftables(Game1.content)[GetDeluxeAutoPetterID()] = DELUXE_AUTO_PETTER_DETAILS; + } + } +} diff --git a/DeluxeAutoPetter/helpers/QuestDetails.cs b/DeluxeAutoPetter/helpers/QuestDetails.cs new file mode 100644 index 0000000..f93a231 --- /dev/null +++ b/DeluxeAutoPetter/helpers/QuestDetails.cs @@ -0,0 +1,202 @@ +using Microsoft.Xna.Framework; +using StardewValley; +using StardewValley.Inventories; +using StardewValley.Menus; + +namespace DeluxeAutoPetter.helpers +{ + internal static class QuestDetails + { + /** ********************** + * Class Variables + ********************** **/ + private static string? QUEST_ID; + private static string? QUEST_DETAILS; + + private static readonly string AUTO_PETTER_ID = "(BC)272"; + private static readonly string HARDWOOD_ID = "(O)709"; + private static readonly string IRIDIUM_BAR_ID = "(O)337"; + + private static readonly string DROPBOX_GAME_LOCATION = "Mountain"; + private static readonly Vector2 DROPBOX_LOCATION = new Vector2(18.5f, 25.5f) * Game1.tileSize; + private static readonly Vector2 DROPBOX_INDICATOR_LOCATION = new Vector2(DROPBOX_LOCATION.X - 3, DROPBOX_LOCATION.Y - Game1.tileSize); // the indicator is 6px wide, so -3px to center it + private static readonly Rectangle DROPBOX_BOUNDING_BOX = new((int)DROPBOX_LOCATION.X - (int)(Game1.tileSize * 1.5), (int)DROPBOX_LOCATION.Y - (int)(Game1.tileSize * 2.5), Game1.tileSize * 3, Game1.tileSize * 3); + + private static readonly Dictionary DONATION_REQUIREMENTS = new() + { + { AUTO_PETTER_ID, 1 }, + { HARDWOOD_ID, 300 }, + { IRIDIUM_BAR_ID, 25 } + }; + + private static MultiplayerHandler.QuestData? QUEST_DATA; + private static Inventory? DONATED_ITEMS; + + /** ********************** + * Variable Getters + ********************** **/ + // ID Getters + public static string GetAutoPetterID() + { + return AUTO_PETTER_ID; + } + + public static string GetHardwoodID() + { + return HARDWOOD_ID; + } + + public static string GetIridiumBarID() + { + return IRIDIUM_BAR_ID; + } + + public static string GetQuestID() + { + if (QUEST_ID is null) throw new ArgumentNullException($"{nameof(QUEST_ID)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + return QUEST_ID; + } + + public static string GetQuestDetails() + { + if (QUEST_DETAILS is null) throw new ArgumentNullException($"{nameof(QUEST_DETAILS)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + return QUEST_DETAILS; + } + + // Data Getters + public static string GetDropBoxGameLocationString() + { + return DROPBOX_GAME_LOCATION; + } + + public static bool GetIsTriggered() + { + if (QUEST_DATA is null) throw new ArgumentNullException($"{nameof(QUEST_DATA)} has not been initialized! The {nameof(LoadQuestData)} method must be called first!"); + + return QUEST_DATA.IsTriggered; + } + + /** ********************** + * Data Setters + ********************** **/ + public static void SetIsTriggered(bool isTriggered) + { + if (QUEST_DATA is null) throw new ArgumentNullException($"{nameof(QUEST_DATA)} has not been initialized! The {nameof(LoadQuestData)} method must be called first!"); + + QUEST_DATA.IsTriggered = isTriggered; + } + + /** ********************** + * Public Methods + ********************** **/ + // Utility methods + public static void Initialize(string modID) + { + QUEST_ID = $"{modID}.Quest"; + QUEST_DETAILS = $"Basic/{I18n.QuestTitle()}/{I18n.QuestDescription()}/{I18n.QuestObjective()}/null/-1/0/-1/false"; + } + + public static void LoadQuestData(long playerID) + { + QUEST_DATA = MultiplayerHandler.GetPlayerQuestData(playerID); + CreateDonatedInventory(QUEST_DATA.DonationCounts); + } + + public static bool IsQuestDataNull() + { + return QUEST_DATA is null; + } + + public static void LoadQuest() + { + DataLoader.Quests(Game1.content)[GetQuestID()] = GetQuestDetails(); + } + + // Visual Methods + public static void ShowDropboxLocator(bool doShow) + { + Game1.getLocationFromName(DROPBOX_GAME_LOCATION).showDropboxIndicator = doShow; + Game1.getLocationFromName(DROPBOX_GAME_LOCATION).dropBoxIndicatorLocation = DROPBOX_INDICATOR_LOCATION; + } + + public static bool IsMouseOverDropbox(Vector2 mousePosition) + { + return DROPBOX_BOUNDING_BOX.Contains(mousePosition * Game1.tileSize); + } + + public static Vector2 GetInteractionDistanceFromDropboxVector(Vector2 playerInteractionPosition) + { + return playerInteractionPosition - DROPBOX_LOCATION; + } + + public static QuestContainerMenu CreateQuestContainerMenu() + { + if (DONATED_ITEMS is null) throw new ArgumentNullException($"{nameof(DONATED_ITEMS)} has not been initialized! The {nameof(LoadQuestData)} method must be called first!"); + + return new QuestContainerMenu(DONATED_ITEMS, 1, HighlightAcceptableItems, GetAcceptCount, null, UpdateDonationCounts); + } + + /** ********************** + * Private Methods + ********************** **/ + private static bool HighlightAcceptableItems(Item item) + { + return DONATION_REQUIREMENTS.ContainsKey(item.QualifiedItemId); + } + + private static int GetAcceptCount(Item item) + { + if (DONATED_ITEMS is null) throw new ArgumentNullException($"{nameof(DONATED_ITEMS)} has not been initialized! The {nameof(LoadQuestData)} method must be called first!"); + + if (!HighlightAcceptableItems(item)) return 0; // basically means 'if not valid, then return 0' + + int totalNeeded = DONATION_REQUIREMENTS[item.QualifiedItemId]; + int donatedCount = DONATED_ITEMS.FirstOrDefault(donatedItem => donatedItem is not null && donatedItem.QualifiedItemId.Equals(item.QualifiedItemId), null)?.Stack ?? 0; + + return Math.Min(totalNeeded - donatedCount, item.Stack); + } + + private static void UpdateDonationCounts() + { + if (DONATED_ITEMS is null) throw new ArgumentNullException($"{nameof(DONATED_ITEMS)} has not been initialized! The {nameof(LoadQuestData)} method must be called first!"); + if (QUEST_DATA is null) throw new ArgumentNullException($"{nameof(QUEST_DATA)} has not been initialized! The {nameof(LoadQuestData)} method must be called first!"); + + foreach (Item? item in DONATED_ITEMS) + if (item is not null) QUEST_DATA.DonationCounts[item.QualifiedItemId] = item.Stack; + + if (AreDonationRequirementsMet()) + { + Game1.player.completeQuest(QUEST_ID); + ShowDropboxLocator(false); + Game1.player.mailForTomorrow.Add(QuestMail.GetQuestRewardMailID()); + } + } + + private static bool AreDonationRequirementsMet() + { + if (DONATED_ITEMS is null) throw new ArgumentNullException($"{nameof(DONATED_ITEMS)} has not been initialized! The {nameof(LoadQuestData)} method must be called first!"); + + bool completed = true; // assume quest is completed + int i = DONATED_ITEMS.Count - 1; + while (i >= 0 && completed) + { + Item? item = DONATED_ITEMS[i]; + if (item is null || item.Stack != DONATION_REQUIREMENTS[item.QualifiedItemId]) completed = false; + else i--; + } + + return completed; + } + + private static void CreateDonatedInventory(Dictionary donatedDetails) + { + Item? autoPetter = donatedDetails[AUTO_PETTER_ID] <= 0 ? null : ItemRegistry.Create(AUTO_PETTER_ID, donatedDetails[AUTO_PETTER_ID]); + Item? hardwood = donatedDetails[HARDWOOD_ID] <= 0 ? null : ItemRegistry.Create(HARDWOOD_ID, donatedDetails[HARDWOOD_ID]); + Item? iridiumBars = donatedDetails[IRIDIUM_BAR_ID] <= 0 ? null : ItemRegistry.Create(IRIDIUM_BAR_ID, donatedDetails[IRIDIUM_BAR_ID]); + + DONATED_ITEMS = new() { autoPetter, hardwood, iridiumBars }; + } + } +} diff --git a/DeluxeAutoPetter/helpers/QuestMail.cs b/DeluxeAutoPetter/helpers/QuestMail.cs new file mode 100644 index 0000000..09129d2 --- /dev/null +++ b/DeluxeAutoPetter/helpers/QuestMail.cs @@ -0,0 +1,62 @@ +using StardewValley; + +namespace DeluxeAutoPetter.helpers +{ + internal static class QuestMail + { + /** ********************** + * Class Variables + ********************** **/ + private static string? QUEST_MAIL_ID; + private static string? QUEST_REWARD_MAIL_ID; + private static string? QUEST_MAIL_DETAILS; + private static string? QUEST_REWARD_MAIL_DETAILS; + + /** ********************** + * Variable Getters + ********************** **/ + public static string GetQuestMailID() + { + if (QUEST_MAIL_ID is null) throw new ArgumentNullException($"{nameof(QUEST_MAIL_ID)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + return QUEST_MAIL_ID; + } + + public static string GetQuestRewardMailID() + { + if (QUEST_REWARD_MAIL_ID is null) throw new ArgumentNullException($"{nameof(QUEST_REWARD_MAIL_ID)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + return QUEST_REWARD_MAIL_ID; + } + + public static string GetQuestMailDetails() + { + if (QUEST_MAIL_DETAILS is null) throw new ArgumentNullException($"{nameof(QUEST_MAIL_DETAILS)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + return QUEST_MAIL_DETAILS; + } + + public static string GetQuestRewardMailDetails() + { + if (QUEST_REWARD_MAIL_DETAILS is null) throw new ArgumentNullException($"{nameof(QUEST_REWARD_MAIL_DETAILS)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + return QUEST_REWARD_MAIL_DETAILS; + } + + /** ********************** + * Public Methods + ********************** **/ + public static void Initialize(string modID) + { + QUEST_MAIL_ID = $"{modID}.Mail0"; + QUEST_REWARD_MAIL_ID = $"{modID}.Mail1"; + QUEST_MAIL_DETAILS = $"{I18n.QuestMailStart()}^{I18n.QuestMailDetails()}^ - {I18n.RobinName()} %item quest {QuestDetails.GetQuestID()} %%[#]{I18n.QuestMailTitle()}"; + QUEST_REWARD_MAIL_DETAILS = $"@,^{I18n.QuestRewardMailDetails()}^ - {I18n.MarnieName()} [#]{I18n.QuestRewardMailTitle()}"; + } + + public static void LoadMail(string mailID, string mailDetails) + { + DataLoader.Mail(Game1.content)[mailID] = mailDetails; + } + } +} diff --git a/DeluxeAutoPetter/helpers/ShopDetails.cs b/DeluxeAutoPetter/helpers/ShopDetails.cs new file mode 100644 index 0000000..105cb12 --- /dev/null +++ b/DeluxeAutoPetter/helpers/ShopDetails.cs @@ -0,0 +1,29 @@ +using StardewValley; +using StardewValley.GameData.Shops; + +namespace DeluxeAutoPetter.helpers +{ + internal static class ShopDetails + { + private static ShopItemData? DELUXE_AUTO_PETTER_SHOP_DATA; + + public static void Initialize() + { + DELUXE_AUTO_PETTER_SHOP_DATA = new() + { + Id = $"(BC){ObjectDetails.GetDeluxeAutoPetterID()}", + ItemId = $"(BC){ObjectDetails.GetDeluxeAutoPetterID()}", + Condition = $"PLAYER_HAS_MAIL Current {QuestMail.GetQuestRewardMailID()} Received", + Price = 75000 + }; + } + + public static void LoadShopItem() + { + if (DELUXE_AUTO_PETTER_SHOP_DATA is null) throw new ArgumentNullException($"{nameof(DELUXE_AUTO_PETTER_SHOP_DATA)} has not been initialized! The {nameof(Initialize)} method must be called first!"); + + if (!DataLoader.Shops(Game1.content)["AnimalShop"].Items.Any(itemData => itemData.Id.Equals($"(BC){ObjectDetails.GetDeluxeAutoPetterID()}"))) + DataLoader.Shops(Game1.content)["AnimalShop"].Items.Add(DELUXE_AUTO_PETTER_SHOP_DATA); + } + } +} diff --git a/DeluxeAutoPetter/i18n/default.json b/DeluxeAutoPetter/i18n/default.json new file mode 100644 index 0000000..1850644 --- /dev/null +++ b/DeluxeAutoPetter/i18n/default.json @@ -0,0 +1,14 @@ +{ + "quest_mail_title": "Auto-Petter Improvements", + "quest_mail_start": "Hey there!", + "quest_mail_details": "I heard you found a Joja Auto-Petter. If you want, I could look into improving that for you. Just drop off the Auto-Petter, 300 Hardwood, and 25 Iridium Bars in my garage, and I will handle the rest!", + "quest_reward_mail_title": "The Deluxe Auto-Petter", + "quest_reward_mail_details": "Good news! Robin was able to create the \"Deluxe Auto-Petter\" with the materials that you provided! I am now selling these Deluxe Auto-Petters, so come buy one when you have a chance. Your animals will love it!", + "quest_title": "The Deluxe Auto-Petter", + "quest_description": "Deliver the necessary materials to Robin's house.", + "quest_objective": "Deliver 1 Auto-Petter, 300 Hardwood, and 25 Iridium Bars", + "deluxe_auto_petter_display_name": "Deluxe Auto-Petter", + "deluxe_auto_petter_description": "Behaves similarly to a regular Auto-Petter, but with better results! Hooray!", + "robin_name": "Robin", + "marnie_name": "Marnie" +} \ No newline at end of file diff --git a/DeluxeAutoPetter/manifest.json b/DeluxeAutoPetter/manifest.json new file mode 100644 index 0000000..eb4c399 --- /dev/null +++ b/DeluxeAutoPetter/manifest.json @@ -0,0 +1,10 @@ +{ + "Name": "Deluxe Auto-Petter", + "Author": "Frostiverse", + "Version": "1.0.0", + "Description": "Adds a new quest which results with Marnie selling a lore-friendly Deluxe Auto-Petter.", + "UniqueID": "Frostiverse.DeluxeAutoPetter", + "EntryDll": "DeluxeAutoPetter.dll", + "MinimumApiVersion": "3.0.0", + "UpdateKeys": [ "Nexus:23226" ] +} \ No newline at end of file