using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using UIInfoSuite.Extensions; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; using StardewValley.Quests; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace UIInfoSuite.UIElements { class LocationOfTownsfolk : IDisposable { #region Members private List _townsfolk = new List(); private List _checkboxes = new List(); private const int SocialPanelWidth = 190; private const int SocialPanelXOffset = 160; private SocialPage _socialPage; private String[] _friendNames; private readonly IDictionary _options; private readonly IModHelper _helper; private static readonly Dictionary> _mapLocations = new Dictionary>() { { "HarveyRoom", new KeyValuePair(677, 304) }, { "BathHouse_Pool", new KeyValuePair(576, 60) }, { "WizardHouseBasement", new KeyValuePair(196, 352) }, { "BugLand", new KeyValuePair(0, 0) }, { "Desert", new KeyValuePair(60, 92) }, { "Cellar", new KeyValuePair(0, 0) }, { "JojaMart", new KeyValuePair(872, 280) }, { "Tent", new KeyValuePair(784, 128) }, { "HaleyHouse", new KeyValuePair(652, 408) }, { "Hospital", new KeyValuePair(677, 304) }, { "FarmHouse", new KeyValuePair(470, 260) }, { "Farm", new KeyValuePair(470, 260) }, { "ScienceHouse", new KeyValuePair(732, 148) }, { "ManorHouse", new KeyValuePair(768, 395) }, { "AdventureGuild", new KeyValuePair(0, 0) }, { "SeedShop", new KeyValuePair(696, 296) }, { "Blacksmith", new KeyValuePair(852, 388) }, { "JoshHouse", new KeyValuePair(740, 320) }, { "SandyHouse", new KeyValuePair(40, 40) }, { "Tunnel", new KeyValuePair(0, 0) }, { "CommunityCenter", new KeyValuePair(692, 204) }, { "Backwoods", new KeyValuePair(460, 156) }, { "ElliottHouse", new KeyValuePair(826, 550) }, { "SebastianRoom", new KeyValuePair(732, 148) }, { "BathHouse_Entry", new KeyValuePair(576, 60) }, { "Greenhouse", new KeyValuePair(0, 0) }, { "Sewer", new KeyValuePair(380, 596) }, { "WizardHouse", new KeyValuePair(196, 352) }, { "Trailer", new KeyValuePair(780, 360) }, { "pamHouseUpgrade", new KeyValuePair(780, 360) }, { "Forest", new KeyValuePair(80, 272) }, { "Woods", new KeyValuePair(100, 272) }, { "WitchSwamp", new KeyValuePair(0, 0) }, { "ArchaeologyHouse", new KeyValuePair(892, 416) }, { "FishShop", new KeyValuePair(844, 608) }, { "Saloon", new KeyValuePair(714, 354) }, { "LeahHouse", new KeyValuePair(452, 436) }, { "Town", new KeyValuePair(680, 360) }, { "Mountain", new KeyValuePair(762, 154) }, { "BusStop", new KeyValuePair(516, 224) }, { "Railroad", new KeyValuePair(644, 64) }, { "SkullCave", new KeyValuePair(0, 0) }, { "BathHouse_WomensLocker", new KeyValuePair(576, 60) }, { "Beach", new KeyValuePair(790, 550) }, { "BathHouse_MensLocker", new KeyValuePair(576, 60) }, { "Mine", new KeyValuePair(880, 100) }, { "WitchHut", new KeyValuePair(0, 0) }, { "AnimalShop", new KeyValuePair(420, 392) }, { "SamHouse", new KeyValuePair(612, 396) }, { "WitchWarpCave", new KeyValuePair(0, 0) }, { "Club", new KeyValuePair(60, 92) } }; #endregion public LocationOfTownsfolk(IModHelper helper, IDictionary options) { _helper = helper; _options = options; } public void Dispose() { ToggleShowNPCLocationsOnMap(false); } public void ToggleShowNPCLocationsOnMap(bool showLocations) { ExtendMenuIfNeeded(); _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu_DrawSocialPageOptions; _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu_DrawNPCLocationsOnMap; _helper.Events.Input.ButtonPressed -= OnButtonPressed_ForSocialPage; _helper.Events.Display.MenuChanged -= OnMenuChanged; if (showLocations) { _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu_DrawSocialPageOptions; _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu_DrawNPCLocationsOnMap; _helper.Events.Input.ButtonPressed += OnButtonPressed_ForSocialPage; _helper.Events.Display.MenuChanged += OnMenuChanged; } } /// Raised after a game menu is opened, closed, or replaced. /// The event sender. /// The event arguments. private void OnMenuChanged(object sender, MenuChangedEventArgs e) { ExtendMenuIfNeeded(); } private void ExtendMenuIfNeeded() { if (Game1.activeClickableMenu is GameMenu gameMenu) { foreach (var menu in gameMenu.pages) { if (menu is SocialPage socialPage) { _socialPage = socialPage; _friendNames = _socialPage.names .Select(name => name.ToString()) .ToArray(); break; } } _townsfolk.Clear(); foreach (var location in Game1.locations) { foreach (var npc in location.characters) { if (Game1.player.friendshipData.ContainsKey(npc.Name)) _townsfolk.Add(npc); } } _checkboxes.Clear(); foreach (var friendName in _friendNames) { int hashCode = friendName.GetHashCode(); OptionsCheckbox checkbox = new OptionsCheckbox("", hashCode); _checkboxes.Add(checkbox); //default to on bool optionForThisFriend = true; if (!Game1.player.friendshipData.ContainsKey(friendName)) { checkbox.greyedOut = true; optionForThisFriend = false; } else { String optionValue = _options.SafeGet(hashCode.ToString()); if (String.IsNullOrEmpty(optionValue)) { _options[hashCode.ToString()] = optionForThisFriend.ToString(); } else { optionForThisFriend = optionValue.SafeParseBool(); } } checkbox.isChecked = optionForThisFriend; } } } /// Raised after the player presses a button on the keyboard, controller, or mouse. /// The event sender. /// The event arguments. private void OnButtonPressed_ForSocialPage(object sender, ButtonPressedEventArgs e) { if (Game1.activeClickableMenu is GameMenu && (e.Button == SButton.MouseLeft || e.Button == SButton.ControllerA)) { CheckSelectedBox(); } } private void CheckSelectedBox() { if (Game1.activeClickableMenu is GameMenu gameMenu) { int slotPosition = (int)typeof(SocialPage) .GetField("slotPosition", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(_socialPage); for (int i = slotPosition; i < slotPosition + 5; ++i) { OptionsCheckbox checkbox = _checkboxes[i]; if (checkbox.bounds.Contains(Game1.getMouseX(), Game1.getMouseY()) && !checkbox.greyedOut) { checkbox.isChecked = !checkbox.isChecked; _options[checkbox.whichOption.ToString()] = checkbox.isChecked.ToString(); Game1.playSound("drumkit6"); } } } } /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. /// The event sender. /// The event arguments. private void OnRenderedActiveMenu_DrawNPCLocationsOnMap(object sender, RenderedActiveMenuEventArgs e) { if (Game1.activeClickableMenu is GameMenu gameMenu) { if (gameMenu.currentTab == 3) { List namesToShow = new List(); foreach (var character in _townsfolk) { try { int hashCode = character.Name.GetHashCode(); bool drawCharacter = _options.SafeGet(hashCode.ToString()).SafeParseBool(); if (drawCharacter) { KeyValuePair location = new KeyValuePair((int)character.Position.X, (int)character.position.Y); String locationName = character.currentLocation?.Name ?? character.DefaultMap; switch (locationName) { case "Town": case "Forest": { int xStart = 0; int yStart = 0; int areaWidth = 0; int areaHeight = 0; switch (locationName) { case "Town": { xStart = 595; yStart = 163; areaWidth = 345; areaHeight = 330; break; } case "Forest": { xStart = 183; yStart = 378; areaWidth = 319; areaHeight = 261; break; } } xTile.Map map = character.currentLocation.Map; float xScale = (float)areaWidth / (float)map.DisplayWidth; float yScale = (float)areaHeight / (float)map.DisplayHeight; float scaledX = character.position.X * xScale; float scaledY = character.position.Y * yScale; int xPos = (int)scaledX + xStart; int yPos = (int)scaledY + yStart; location = new KeyValuePair(xPos, yPos); break; } default: { _mapLocations.TryGetValue(locationName, out location); break; } } //if (character.currentLocation.Name == "Town") //{ // String locationName = character.currentLocation.Name; // xTile.Map map = character.currentLocation.Map; // int xStart = 595; // int yStart = 163; // int townWidth = 345; // int townHeight = 330; // float xScale = (float)townWidth / (float)map.DisplayWidth; // float yScale = (float)townHeight / (float)map.DisplayHeight; // float scaledX = character.position.X * xScale; // float scaledY = character.position.Y * yScale; // int xPos = (int)scaledX + xStart; // int yPos = (int)scaledY + yStart; // location = new KeyValuePair(xPos, yPos); //} //else //{ // _mapLocations.TryGetValue(character.currentLocation.name, out location); //} Rectangle headShot = character.GetHeadShot(); int xBase = Game1.activeClickableMenu.xPositionOnScreen - 158; int yBase = Game1.activeClickableMenu.yPositionOnScreen - 40; int x = xBase + location.Key; int y = yBase + location.Value; Color color = character.CurrentDialogue.Count <= 0 ? Color.Gray : Color.White; ClickableTextureComponent textureComponent = new ClickableTextureComponent( character.Name, new Rectangle(x, y, 0, 0), null, character.Name, character.Sprite.Texture, headShot, 2.3f); float headShotScale = 2f; Game1.spriteBatch.Draw( character.Sprite.Texture, new Vector2(x, y), new Rectangle?(headShot), color, 0.0f, Vector2.Zero, headShotScale, SpriteEffects.None, 1f); int mouseX = Game1.getMouseX(); int mouseY = Game1.getMouseY(); if (mouseX >= x && mouseX <= x + headShot.Width * headShotScale && mouseY >= y && mouseY <= y + headShot.Height * headShotScale) { namesToShow.Add(character.displayName); } foreach (var quest in Game1.player.questLog) { if (quest.accepted.Value && quest.dailyQuest.Value && !quest.completed.Value) { bool isQuestTarget = false; switch (quest.questType.Value) { case 3: isQuestTarget = (quest as ItemDeliveryQuest).target.Value == character.Name; break; case 4: isQuestTarget = (quest as SlayMonsterQuest).target.Value == character.Name; break; case 7: isQuestTarget = (quest as FishingQuest).target.Value == character.Name; break; case 10: isQuestTarget = (quest as ResourceCollectionQuest).target.Value == character.Name; break; } if (isQuestTarget) Game1.spriteBatch.Draw( Game1.mouseCursors, new Vector2(x + 10, y - 12), new Rectangle(394, 495, 4, 10), Color.White, 0.0f, Vector2.Zero, 3f, SpriteEffects.None, 1f); } } } } catch (Exception ex) { ModEntry.MonitorObject.Log(ex.Message + Environment.NewLine + ex.StackTrace, LogLevel.Error); } } if (namesToShow.Count > 0) { StringBuilder text = new StringBuilder(); int longestLength = 0; foreach (String name in namesToShow) { text.AppendLine(name); longestLength = Math.Max(longestLength, (int)Math.Ceiling(Game1.smallFont.MeasureString(name).Length())); } int windowHeight = Game1.smallFont.LineSpacing * namesToShow.Count + 25; Vector2 windowPos = new Vector2(Game1.getMouseX() + 40, Game1.getMouseY() - windowHeight); IClickableMenu.drawTextureBox( Game1.spriteBatch, (int)windowPos.X, (int)windowPos.Y, longestLength + 30, Game1.smallFont.LineSpacing * namesToShow.Count + 25, Color.White); Game1.spriteBatch.DrawString( Game1.smallFont, text, new Vector2(windowPos.X + 17, windowPos.Y + 17), Game1.textShadowColor); Game1.spriteBatch.DrawString( Game1.smallFont, text, new Vector2(windowPos.X + 15, windowPos.Y + 15), Game1.textColor); } //The cursor needs to show up in front of the character faces Tools.DrawMouseCursor(); String hoverText = (String)typeof(MapPage) .GetField( "hoverText", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(gameMenu.pages[gameMenu.currentTab]); IClickableMenu.drawHoverText( Game1.spriteBatch, hoverText, Game1.smallFont); } } } /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. /// The event sender. /// The event arguments. private void OnRenderedActiveMenu_DrawSocialPageOptions(object sender, RenderedActiveMenuEventArgs e) { if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == 2) { Game1.drawDialogueBox( Game1.activeClickableMenu.xPositionOnScreen - SocialPanelXOffset, Game1.activeClickableMenu.yPositionOnScreen, SocialPanelWidth, Game1.activeClickableMenu.height, false, true); int slotPosition = (int)typeof(SocialPage) .GetField("slotPosition", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(_socialPage); int yOffset = 0; for (int i = slotPosition; i < slotPosition + 5 && i < _friendNames.Length; ++i) { OptionsCheckbox checkbox = _checkboxes[i]; checkbox.bounds.X = Game1.activeClickableMenu.xPositionOnScreen - 60; checkbox.bounds.Y = Game1.activeClickableMenu.yPositionOnScreen + 130 + yOffset; checkbox.draw(Game1.spriteBatch, 0, 0); yOffset += 112; Color color = checkbox.isChecked ? Color.White : Color.Gray; Game1.spriteBatch.Draw( Game1.mouseCursors, new Vector2(checkbox.bounds.X - 50, checkbox.bounds.Y), new Rectangle(80, 0, 16, 16), color, 0.0f, Vector2.Zero, 3f, SpriteEffects.None, 1f); if (yOffset != 560) { Game1.spriteBatch.Draw( Game1.staminaRect, new Rectangle( checkbox.bounds.X - 50, checkbox.bounds.Y + 72, SocialPanelWidth / 2 - 6, 4), Color.SaddleBrown); Game1.spriteBatch.Draw( Game1.staminaRect, new Rectangle( checkbox.bounds.X - 50, checkbox.bounds.Y + 76, SocialPanelWidth / 2 - 6, 4), Color.BurlyWood); } if (!Game1.options.hardwareCursor) { Game1.spriteBatch.Draw( Game1.mouseCursors, new Vector2( Game1.getMouseX(), Game1.getMouseY()), Game1.getSourceRectForStandardTileSheet( Game1.mouseCursors, Game1.mouseCursor, 16, 16), Color.White, 0.0f, Vector2.Zero, Game1.pixelZoom + (Game1.dialogueButtonScale / 150.0f), SpriteEffects.None, 1f); } if (checkbox.bounds.Contains(Game1.getMouseX(), Game1.getMouseY())) IClickableMenu.drawHoverText( Game1.spriteBatch, "Track on map", Game1.dialogueFont); } } } } }