From ea003ce3420214b94cdbe44124bdc69bd4569127 Mon Sep 17 00:00:00 2001 From: b3nk3lly Date: Sun, 22 Sep 2024 00:40:54 -0300 Subject: [PATCH 1/4] Add schedule to NPC lookup --- LookupAnything/DataParser.cs | 64 ++++++++--------- .../Framework/Fields/ScheduleField.cs | 68 +++++++++++++++++++ .../Lookups/Characters/CharacterSubject.cs | 6 ++ LookupAnything/GameHelper.cs | 10 +++ LookupAnything/i18n/de.json | 6 ++ LookupAnything/i18n/default.json | 6 ++ LookupAnything/i18n/es.json | 6 ++ LookupAnything/i18n/fr.json | 6 ++ LookupAnything/i18n/hu.json | 6 ++ LookupAnything/i18n/it.json | 6 ++ LookupAnything/i18n/ja.json | 6 ++ LookupAnything/i18n/ko.json | 6 ++ LookupAnything/i18n/pl.json | 6 ++ LookupAnything/i18n/pt.json | 6 ++ LookupAnything/i18n/ru.json | 6 ++ LookupAnything/i18n/th.json | 6 ++ LookupAnything/i18n/tr.json | 6 ++ LookupAnything/i18n/uk.json | 6 ++ LookupAnything/i18n/zh.json | 6 ++ 19 files changed, 206 insertions(+), 32 deletions(-) create mode 100644 LookupAnything/Framework/Fields/ScheduleField.cs diff --git a/LookupAnything/DataParser.cs b/LookupAnything/DataParser.cs index 05d8105c8..b1c2acb25 100644 --- a/LookupAnything/DataParser.cs +++ b/LookupAnything/DataParser.cs @@ -324,6 +324,38 @@ public string GetLocationDisplayName(FishSpawnLocationData fishSpawnData) return this.GetLocationDisplayName(fishSpawnData.LocationId, locationData, fishSpawnData.Area); } + /// Get the translated display name for a location and optional fish area. + /// The location's internal name. + /// The location data, if available. + /// The fish area ID within the location, if applicable. + public string GetLocationDisplayName(string id, LocationData? data, string? fishAreaId) + { + // special cases + { + // skip: no area set + if (string.IsNullOrWhiteSpace(fishAreaId)) + return this.GetLocationDisplayName(id, data); + + // special case: mine level + if (string.Equals(id, "UndergroundMine", StringComparison.OrdinalIgnoreCase)) + return I18n.Location_UndergroundMine_Level(level: fishAreaId); + } + + // get base data + string locationName = this.GetLocationDisplayName(id, data); + string areaName = TokenParser.ParseText(data?.FishAreas?.GetValueOrDefault(fishAreaId)?.DisplayName); + + // build translation + string displayName = I18n.GetByKey($"location.{id}.{fishAreaId}", new { locationName }).UsePlaceholder(false); // predefined translation + if (string.IsNullOrWhiteSpace(displayName)) + { + displayName = !string.IsNullOrWhiteSpace(areaName) + ? I18n.Location_FishArea(locationName: locationName, areaName: areaName) + : I18n.Location_UnknownFishArea(locationName: locationName, id: fishAreaId); + } + return displayName; + } + /// Parse monster data. /// Reverse engineered from , , and the constructor. public IEnumerable GetMonsters() @@ -652,38 +684,6 @@ from result in itemQueryResults /********* ** Private methods *********/ - /// Get the translated display name for a location and optional fish area. - /// The location's internal name. - /// The location data, if available. - /// The fish area ID within the location, if applicable. - private string GetLocationDisplayName(string id, LocationData? data, string? fishAreaId) - { - // special cases - { - // skip: no area set - if (string.IsNullOrWhiteSpace(fishAreaId)) - return this.GetLocationDisplayName(id, data); - - // special case: mine level - if (string.Equals(id, "UndergroundMine", StringComparison.OrdinalIgnoreCase)) - return I18n.Location_UndergroundMine_Level(level: fishAreaId); - } - - // get base data - string locationName = this.GetLocationDisplayName(id, data); - string areaName = TokenParser.ParseText(data?.FishAreas?.GetValueOrDefault(fishAreaId)?.DisplayName); - - // build translation - string displayName = I18n.GetByKey($"location.{id}.{fishAreaId}", new { locationName }).UsePlaceholder(false); // predefined translation - if (string.IsNullOrWhiteSpace(displayName)) - { - displayName = !string.IsNullOrWhiteSpace(areaName) - ? I18n.Location_FishArea(locationName: locationName, areaName: areaName) - : I18n.Location_UnknownFishArea(locationName: locationName, id: fishAreaId); - } - return displayName; - } - /// Get the translated display name for a location. /// The location's internal name. /// The location data, if available. diff --git a/LookupAnything/Framework/Fields/ScheduleField.cs b/LookupAnything/Framework/Fields/ScheduleField.cs new file mode 100644 index 000000000..b13c3f54c --- /dev/null +++ b/LookupAnything/Framework/Fields/ScheduleField.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Pathfinding; + +namespace Pathoschild.Stardew.LookupAnything.Framework.Fields +{ + /// A metadata field which shows an NPC's schedule. + /// The NPC's loaded schedule. + /// Provides utility methods for interacting with the game code. + internal class ScheduleField(Dictionary schedule, GameHelper gameHelper) : GenericField(I18n.Npc_Schedule(), GetText(schedule, gameHelper)) + { + /// + public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth) + { + float topOffset = 0; + + foreach (IFormattedText text in this.Value) + { + topOffset += spriteBatch.DrawTextBlock(font, [text], new Vector2(position.X, position.Y + topOffset), wrapWidth).Y; + } + + return new Vector2(wrapWidth, topOffset); + } + + /// Get the text to display. + /// An NPC's loaded schedule. + /// Provides utility methods for interacting with the game code. + private static IEnumerable GetText(Dictionary schedule, GameHelper gameHelper) + { + var formattedSchedule = FormatSchedule(schedule).ToList(); + + for (int i = 0; i < formattedSchedule.Count; i++) + { + (int time, SchedulePathDescription entry) = formattedSchedule[i]; + + string timeString = formattedSchedule.Count == 1 ? I18n.Npc_Schedule_AllDay() : Game1.getTimeOfDayString(time); + string locationDisplayName = gameHelper.GetLocationDisplayName(entry.targetLocationName, Game1.getLocationFromName(entry.targetLocationName).GetData()); + + // check if the current game time is between current and next schedule entry + bool isHappeningNow = Game1.timeOfDay >= time && (i >= formattedSchedule.Count - 1 || Game1.timeOfDay < formattedSchedule[i + 1].time); + Color textColor = isHappeningNow ? Color.Black : Color.Gray; + + yield return new FormattedText($"{timeString} - {locationDisplayName}", textColor); + } + } + + /// Returns a collection of schedule entries sorted by time. Consecutive entries with the same target location are omitted. + /// The schedule to format. + private static IEnumerable<(int time, SchedulePathDescription entry)> FormatSchedule(Dictionary schedule) + { + List sortedKeys = [.. schedule.Keys.OrderBy(key => key)]; + string prevTargetLocationName = string.Empty; + + foreach (int time in sortedKeys) + { + // skip if the entry does not exist or the previous entry was for the same location + if (!schedule.TryGetValue(time, out SchedulePathDescription? entry) || entry.targetLocationName == prevTargetLocationName) + continue; + + prevTargetLocationName = entry.targetLocationName; + yield return (time, entry); + } + } + } +} diff --git a/LookupAnything/Framework/Lookups/Characters/CharacterSubject.cs b/LookupAnything/Framework/Lookups/Characters/CharacterSubject.cs index d89a155bf..24e9ea9f1 100644 --- a/LookupAnything/Framework/Lookups/Characters/CharacterSubject.cs +++ b/LookupAnything/Framework/Lookups/Characters/CharacterSubject.cs @@ -400,6 +400,12 @@ private IEnumerable GetDataForVillager(NPC npc) if (this.ShowGiftTastes.Hated) yield return this.GetGiftTasteField(I18n.Npc_HatesGifts(), giftTastes, ownedItems, GiftTaste.Hate); } + + // schedule + if (npc.TryLoadSchedule()) + { + yield return new ScheduleField(npc.Schedule, this.GameHelper); + } } } diff --git a/LookupAnything/GameHelper.cs b/LookupAnything/GameHelper.cs index ba8eb4692..14d11d26d 100644 --- a/LookupAnything/GameHelper.cs +++ b/LookupAnything/GameHelper.cs @@ -28,6 +28,7 @@ using StardewValley.GameData.Crafting; using StardewValley.GameData.Crops; using StardewValley.GameData.FishPonds; +using StardewValley.GameData.Locations; using StardewValley.ItemTypeDefinitions; using StardewValley.Locations; using StardewValley.Menus; @@ -375,6 +376,15 @@ public string GetLocationDisplayName(FishSpawnLocationData fishSpawnData) return this.DataParser.GetLocationDisplayName(fishSpawnData); } + /// Get the translated display name for a location and optional fish area. + /// The location's internal name. + /// The location data. + /// The fish area ID within the location, if applicable. + public string GetLocationDisplayName(string id, LocationData data, string? fishAreaId = null) + { + return this.DataParser.GetLocationDisplayName(id, data, fishAreaId); + } + /// Parse monster data. public IEnumerable GetMonsterData() { diff --git a/LookupAnything/i18n/de.json b/LookupAnything/i18n/de.json index 45f4bc8b4..7da0c5ef8 100644 --- a/LookupAnything/i18n/de.json +++ b/LookupAnything/i18n/de.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "Waldfluss (Südspitze der großen Insel)", "location.forest.lake": "Waldteich", "location.forest.river": "Waldfluss", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "Nachtmarkt Tiefsee-U-Boot", "location.town.northmost-bridge": "Stadt (Nord-östliche Brücke)", "location.undergroundMine": "Mine", @@ -491,6 +495,7 @@ "npc.neutral-gifts": "Neutrale Geschenke", "npc.dislikes-gifts": "Nicht gemochte Geschenke", "npc.hates-gifts": "Gehasste Geschenke", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Du bist verheiratet! <", // < turns into a heart @@ -500,6 +505,7 @@ "npc.friendship.need-points": "nächstes in {{count}} Punkten", "npc.undiscovered-gift-taste": "{{count}} unaufgedeckte Gegenstände", "npc.unowned-gift-taste": "{{count}} nicht bessesene Gegenstände", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/default.json b/LookupAnything/i18n/default.json index 93f2d3add..750b0e9b8 100644 --- a/LookupAnything/i18n/default.json +++ b/LookupAnything/i18n/default.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "{{locationName}} (river near south tip of large island)", "location.forest.lake": "{{locationName}} (pond)", "location.forest.river": "{{locationName}} (river)", + "location.HarveyRoom": "Harvey's Room", + "location.LeoTreeHouse": "Treehouse", + "location.SandyHouse": "Oasis", + "location.SebastianRoom": "Sebastian's Room", "location.submarine": "Night Market Submarine", "location.town.northmost-bridge": "{{locationName}} (northmost bridge)", "location.undergroundMine": "Mines", @@ -491,6 +495,7 @@ "npc.neutral-gifts": "Neutral gifts", "npc.dislikes-gifts": "Dislikes gifts", "npc.hates-gifts": "Hates gifts", + "npc.schedule": "Today's schedule", // values "npc.can-romance.married": "You're married! <", // < turns into a heart @@ -500,6 +505,7 @@ "npc.friendship.need-points": "next in {{count}} pts", "npc.undiscovered-gift-taste": "{{count}} unrevealed items", "npc.unowned-gift-taste": "{{count}} unowned items", + "npc.schedule.all-day": "All day", /********* diff --git a/LookupAnything/i18n/es.json b/LookupAnything/i18n/es.json index 0953e3f57..0b6870771 100644 --- a/LookupAnything/i18n/es.json +++ b/LookupAnything/i18n/es.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "río de bosque (en la punta sureña de la gran isla)", "location.forest.lake": "estanque de bosque", "location.forest.river": "río de bosque", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "submarino del Mercado de Noche", "location.town.northmost-bridge": "pueblo (en el puente más al norte)", "location.undergroundMine": "minas", @@ -492,6 +496,7 @@ "npc.neutral-gifts": "Regalos neutrales", "npc.dislikes-gifts": "Regalos que no le gustaron", "npc.hates-gifts": "Regalos odiados", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "¡Estás casado! <", // < turns into a heart @@ -501,6 +506,7 @@ "npc.friendship.need-points": "siguiente en {{count}} puntos", "npc.undiscovered-gift-taste": "{{count}} items sin revelar", "npc.unowned-gift-taste": "{{count}} objetos no obtenidos", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/fr.json b/LookupAnything/i18n/fr.json index 4718bb347..c705de8fc 100644 --- a/LookupAnything/i18n/fr.json +++ b/LookupAnything/i18n/fr.json @@ -115,6 +115,10 @@ "location.forest.island-tip": "rivière de la forêt (pointe sud de la grande île)", "location.forest.lake": "étang de la forêt", "location.forest.river": "rivère de la forêt", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "sous-marin du marché de nuit", "location.town.northmost-bridge": "ville (pont le plus au nord)", "location.undergroundMine": "mines", @@ -494,6 +498,7 @@ "npc.neutral-gifts": "Cadeaux neutres", "npc.dislikes-gifts": "Cadeaux non appréciés", "npc.hates-gifts": "Cadeaux détestés", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Vous êtes mariés ! <", // < turns into a heart @@ -503,6 +508,7 @@ "npc.friendship.need-points": "manque {{count}} pts", "npc.undiscovered-gift-taste": "{{count}} goût(s) non révelé(s)", "npc.unowned-gift-taste": "{{count}} unowned items", // TODO + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/hu.json b/LookupAnything/i18n/hu.json index 7ab70cbc3..90f054385 100644 --- a/LookupAnything/i18n/hu.json +++ b/LookupAnything/i18n/hu.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "erdei folyó (nagy sziget déli csücske)", "location.forest.lake": "erdei tó", "location.forest.river": "erdei folyó", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "Éjszakai Piac tengeralattjáró", "location.town.northmost-bridge": "város (legészakibb híd)", "location.undergroundMine": "bányák", @@ -494,6 +498,7 @@ "npc.neutral-gifts": "Semleges ajándékok", "npc.dislikes-gifts": "Ajándékok, melyeket nem szeret", "npc.hates-gifts": "Ajándékok, melyeket utál", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Házastársak vagytok! <", // < turns into a heart @@ -503,6 +508,7 @@ "npc.friendship.need-points": "Következő szint {{count}} pont múlva", "npc.undiscovered-gift-taste": "{{count}} fel nem fedezett dolog", "npc.unowned-gift-taste": "{{count}} unowned items", // TODO + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/it.json b/LookupAnything/i18n/it.json index 52e02e34c..d535d2ce5 100644 --- a/LookupAnything/i18n/it.json +++ b/LookupAnything/i18n/it.json @@ -112,6 +112,10 @@ "location.forest.island-tip": "fiume nella foresta (punta Sud dell'isola maggiore)", "location.forest.lake": "laghetto nella foresta", "location.forest.river": "fiume nella foresta", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "sottomarino", "location.town.northmost-bridge": "paese (ponte Nord)", "location.undergroundMine": "miniera", @@ -490,6 +494,7 @@ "npc.neutral-gifts": "Regali neutrali", "npc.dislikes-gifts": "Regali che non piacciono", "npc.hates-gifts": "Regali che odia", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Siete sposati! <", // < turns into a heart @@ -499,6 +504,7 @@ "npc.friendship.need-points": "prossimo fra {{count}} pti", "npc.undiscovered-gift-taste": "{{count}} oggetti non rivelati", "npc.unowned-gift-taste": "{{count}} oggetti non posseduti", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/ja.json b/LookupAnything/i18n/ja.json index cb6dc73cb..d8f5b2aeb 100644 --- a/LookupAnything/i18n/ja.json +++ b/LookupAnything/i18n/ja.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "森の川(南の小島の南端)", "location.forest.lake": "森の池", "location.forest.river": "森の川", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "夜の市 潜水艦", "location.town.northmost-bridge": "ペリカンタウン(北端の橋)", "location.undergroundMine": "鉱山", @@ -491,6 +495,7 @@ "npc.neutral-gifts": "普通の贈り物", "npc.dislikes-gifts": "嫌いな贈り物", "npc.hates-gifts": "大嫌いな贈り物", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "あなたと結婚しました!", // < turns into a heart @@ -500,6 +505,7 @@ "npc.friendship.need-points": "次のハートまで{{count}}ポイント必要", "npc.undiscovered-gift-taste": "{{count}}個の贈った事のないアイテム", "npc.unowned-gift-taste": "{{count}}個のアイテムは未所有", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/ko.json b/LookupAnything/i18n/ko.json index 3a35ff906..dfa4c9807 100644 --- a/LookupAnything/i18n/ko.json +++ b/LookupAnything/i18n/ko.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "섬 (큰 섬의 남쪽 끝)", "location.forest.lake": "숲 호수", "location.forest.river": "숲 강", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "야시장 잠수함", "location.town.northmost-bridge": "마을 (최북단의 다리)", "location.undergroundMine": "광산", @@ -491,6 +495,7 @@ "npc.neutral-gifts": "그저그런 선물", "npc.dislikes-gifts": "싫어하는 선물", "npc.hates-gifts": "혐오하는 선물", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "당신은 이미 결혼했다!", // < turns into a heart (Korean does not support emoji) @@ -500,6 +505,7 @@ "npc.friendship.need-points": "호감도 UP까지 {{count}}점", "npc.undiscovered-gift-taste": "공개되지 않은 품목 {{count}}개", "npc.unowned-gift-taste": "소유하지 않은 품목 {{count}}개", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/pl.json b/LookupAnything/i18n/pl.json index 0ee493e16..86e6ee51d 100644 --- a/LookupAnything/i18n/pl.json +++ b/LookupAnything/i18n/pl.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "leśna rzeka (południowy kraniec dużej wyspy)", "location.forest.lake": "leśny staw", "location.forest.river": "leśna rzeka", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "łódź podwodna Nocnego Rynku", "location.town.northmost-bridge": "miasto (kładka na północy)", "location.undergroundMine": "kopalnia", @@ -491,6 +495,7 @@ "npc.neutral-gifts": "Neutralne prezenty", "npc.dislikes-gifts": "Nielubiane prezenty", "npc.hates-gifts": "Znienawidzone prezenty", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Jesteście małżeństwem! <", // < turns into a heart @@ -500,6 +505,7 @@ "npc.friendship.need-points": "następne za {{count}} pkt", "npc.undiscovered-gift-taste": "{{count}} nieodkrytych przedmiotów", "npc.unowned-gift-taste": "{{count}} nieposiadanych przedmiotów", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/pt.json b/LookupAnything/i18n/pt.json index 0be173382..449f4931c 100644 --- a/LookupAnything/i18n/pt.json +++ b/LookupAnything/i18n/pt.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "Rio da Floresta Cinzaseiva (ponta sul da ilha grande)", "location.forest.lake": "Lagoa da Floresta Cinzaseiva", "location.forest.river": "Rio da Floresta Cinzaseiva", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "Submarino de Pesca do Mercado Noturno", "location.town.northmost-bridge": "Cidade (ponte mais ao norte)", "location.undergroundMine": "Minas", @@ -492,6 +496,7 @@ "npc.neutral-gifts": "Presentes neutros", "npc.dislikes-gifts": "Presentes que não gosta", "npc.hates-gifts": "Presentes que odeia", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Você é casado! <", // < turns into a heart @@ -501,6 +506,7 @@ "npc.friendship.need-points": "Precisa de {{count}} pontos para prosseguir", "npc.undiscovered-gift-taste": "{{count}} itens não revelados", "npc.unowned-gift-taste": "{{count}} itens não possuídos", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/ru.json b/LookupAnything/i18n/ru.json index c5b237c6a..51f48b941 100644 --- a/LookupAnything/i18n/ru.json +++ b/LookupAnything/i18n/ru.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "лесная река (южная часть большого острова)", "location.forest.lake": "лесное озеро", "location.forest.river": "лесная река", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "ночной Рынок на подводной лодке", "location.town.northmost-bridge": "город (самый северный мост)", "location.undergroundMine": "шахты", @@ -491,6 +495,7 @@ "npc.neutral-gifts": "Равнодушен к подаркам:", "npc.dislikes-gifts": "Не любит подарки", "npc.hates-gifts": "Ненавидит подарки", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Вы женаты! <", // < turns into a heart @@ -500,6 +505,7 @@ "npc.friendship.need-points": "след. через {{count}}", "npc.undiscovered-gift-taste": "{{count}} нераскрытых предметов", "npc.unowned-gift-taste": "{{count}} бесхозных предметов", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/th.json b/LookupAnything/i18n/th.json index 98fb61bc8..5ce021ad3 100644 --- a/LookupAnything/i18n/th.json +++ b/LookupAnything/i18n/th.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "แม่น้ำในป่า (ปลายทางใต้สุดของเกาะใหญ่กลางแม่น้ำ)", "location.forest.lake": "ทะลสาบในป่า", "location.forest.river": "แม่น้ำในป่า", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "เรือดำน้ำในงานตลาดกลางคืน", "location.town.northmost-bridge": "เมือง (สะพานที่อยู่ทิศเหนือสุดของเมือง)", "location.undergroundMine": "เหมือง", @@ -494,6 +498,7 @@ "npc.neutral-gifts": "ของขวัญที่เฉยๆ", "npc.dislikes-gifts": "ของขวัญที่ไม่ชอบ", "npc.hates-gifts": "ของขวัญที่เกลียด", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "คุณแต่งงานกันแล้ว! <3", // < turns into a heart @@ -503,6 +508,7 @@ "npc.friendship.need-points": "เลื่อนขั้นใน {{count}} แต้ม", "npc.undiscovered-gift-taste": "{{count}} ชิ้นที่ยังไม่เปิดเผย", "npc.unowned-gift-taste": "{{count}} unowned items", // TODO + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/tr.json b/LookupAnything/i18n/tr.json index 8e33b96b3..4699897a5 100644 --- a/LookupAnything/i18n/tr.json +++ b/LookupAnything/i18n/tr.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "Orman Nehri (büyük adanın güney ucu)", "location.forest.lake": "Orman Göleti", "location.forest.river": "Orman Nehri", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "Akşam Pazarı Denizaltısı", "location.town.northmost-bridge": "Kasaba (en kuzeydeki köprü)", "location.undergroundMine": "Maden", @@ -491,6 +495,7 @@ "npc.neutral-gifts": "Nötr hediyeler", "npc.dislikes-gifts": "Beğenilmeyen hediyeler", "npc.hates-gifts": "Nefret edilen hediyeler", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Evlisin! <", // < turns into a heart @@ -500,6 +505,7 @@ "npc.friendship.need-points": "{{count}} puana gelişir", "npc.undiscovered-gift-taste": "{{count}} gizli nesneler", "npc.unowned-gift-taste": "{{count}} sahipsiz öğe", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/uk.json b/LookupAnything/i18n/uk.json index a57ed709c..b74def9eb 100644 --- a/LookupAnything/i18n/uk.json +++ b/LookupAnything/i18n/uk.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "Лісова річка (південна частина великого острова)", "location.forest.lake": "Лісове озеро", "location.forest.river": "Лісова річка", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "Підводний човен Нічного ринку", "location.town.northmost-bridge": "Місто (найпівнічніший міст)", "location.undergroundMine": "Шахта", @@ -492,6 +496,7 @@ "npc.neutral-gifts": "Байдуже ставиться до :", "npc.dislikes-gifts": "Не любить подарунки", "npc.hates-gifts": "Ненавидить подарунки", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "Ви одружені! <", // < turns into a heart @@ -501,6 +506,7 @@ "npc.friendship.need-points": "Наступн. за {{count}}", "npc.undiscovered-gift-taste": "{{count}} незвіданих предметів", "npc.unowned-gift-taste": "{{count}} предметів, якими ви ніколи не володіли", + "npc.schedule.all-day": "All day", // TODO /********* diff --git a/LookupAnything/i18n/zh.json b/LookupAnything/i18n/zh.json index 65636c8e5..c27487a48 100644 --- a/LookupAnything/i18n/zh.json +++ b/LookupAnything/i18n/zh.json @@ -113,6 +113,10 @@ "location.forest.island-tip": "煤矿森林(河流小岛南端)", "location.forest.lake": "煤矿森林(湖泊)", "location.forest.river": "煤矿森林(河流)", + "location.HarveyRoom": "Harvey's Room", // TODO + "location.LeoTreeHouse": "Treehouse", // TODO + "location.SandyHouse": "Oasis", // TODO + "location.SebastianRoom": "Sebastian's Room", // TODO "location.submarine": "夜市潜艇", "location.town.northmost-bridge": "鹈鹕镇(北部桥区)", "location.undergroundMine": "矿井", @@ -492,6 +496,7 @@ "npc.neutral-gifts": "无感", "npc.dislikes-gifts": "不喜欢", "npc.hates-gifts": "讨厌", + "npc.schedule": "Today's schedule", // TODO // values "npc.can-romance.married": "你们已结婚! ♡", // < turns into a heart @@ -501,6 +506,7 @@ "npc.friendship.need-points": "下一等级需要{{count}}点好感度", "npc.undiscovered-gift-taste": "{{count}} 个未揭露的物品", "npc.unowned-gift-taste": "{{count}} 个未拥有的物品", + "npc.schedule.all-day": "All day", // TODO /********* From ca7963db1d72c5b2f0d2c574b3a3834ff2ebfcae Mon Sep 17 00:00:00 2001 From: b3nk3lly Date: Sun, 6 Oct 2024 15:52:28 -0300 Subject: [PATCH 2/4] Show current event in green and future events in black --- LookupAnything/Framework/Fields/ScheduleField.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/LookupAnything/Framework/Fields/ScheduleField.cs b/LookupAnything/Framework/Fields/ScheduleField.cs index b13c3f54c..c18a51d68 100644 --- a/LookupAnything/Framework/Fields/ScheduleField.cs +++ b/LookupAnything/Framework/Fields/ScheduleField.cs @@ -39,9 +39,14 @@ private static IEnumerable GetText(Dictionary= time && (i >= formattedSchedule.Count - 1 || Game1.timeOfDay < formattedSchedule[i + 1].time); - Color textColor = isHappeningNow ? Color.Black : Color.Gray; + bool didCurrentEventStart = Game1.timeOfDay >= time; + bool didNextEventStart = i < formattedSchedule.Count - 1 && Game1.timeOfDay >= formattedSchedule[i + 1].time; + Color textColor; + + if (didCurrentEventStart) + textColor = didNextEventStart ? Color.Gray : Color.Green; + else + textColor = Color.Black; yield return new FormattedText($"{timeString} - {locationDisplayName}", textColor); } From 095482dfe3471282c970bba3147735da210de5fe Mon Sep 17 00:00:00 2001 From: b3nk3lly Date: Sun, 6 Oct 2024 16:08:39 -0300 Subject: [PATCH 3/4] Add ScheduleEntry record --- LookupAnything/Framework/Fields/ScheduleField.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/LookupAnything/Framework/Fields/ScheduleField.cs b/LookupAnything/Framework/Fields/ScheduleField.cs index c18a51d68..9e08da051 100644 --- a/LookupAnything/Framework/Fields/ScheduleField.cs +++ b/LookupAnything/Framework/Fields/ScheduleField.cs @@ -30,7 +30,7 @@ internal class ScheduleField(Dictionary schedule, /// Provides utility methods for interacting with the game code. private static IEnumerable GetText(Dictionary schedule, GameHelper gameHelper) { - var formattedSchedule = FormatSchedule(schedule).ToList(); + List formattedSchedule = FormatSchedule(schedule).ToList(); for (int i = 0; i < formattedSchedule.Count; i++) { @@ -40,7 +40,7 @@ private static IEnumerable GetText(Dictionary= time; - bool didNextEventStart = i < formattedSchedule.Count - 1 && Game1.timeOfDay >= formattedSchedule[i + 1].time; + bool didNextEventStart = i < formattedSchedule.Count - 1 && Game1.timeOfDay >= formattedSchedule[i + 1].Time; Color textColor; if (didCurrentEventStart) @@ -54,7 +54,7 @@ private static IEnumerable GetText(DictionaryReturns a collection of schedule entries sorted by time. Consecutive entries with the same target location are omitted. /// The schedule to format. - private static IEnumerable<(int time, SchedulePathDescription entry)> FormatSchedule(Dictionary schedule) + private static IEnumerable FormatSchedule(Dictionary schedule) { List sortedKeys = [.. schedule.Keys.OrderBy(key => key)]; string prevTargetLocationName = string.Empty; @@ -66,8 +66,13 @@ private static IEnumerable GetText(DictionaryAn entry in an NPC's schedule. + /// The time that the event starts. + /// A description of the event. + private record ScheduleEntry(int Time, SchedulePathDescription Description); } } From 29b5f6a2e3e6dcdb0dbdd2f79860c3be7c0d9892 Mon Sep 17 00:00:00 2001 From: b3nk3lly Date: Sat, 9 Nov 2024 15:11:00 -0400 Subject: [PATCH 4/4] Migrate ScheduleField to file-scoped namespace --- .../Framework/Fields/ScheduleField.cs | 105 +++++++++--------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/LookupAnything/Framework/Fields/ScheduleField.cs b/LookupAnything/Framework/Fields/ScheduleField.cs index 9e08da051..9b07ed3ca 100644 --- a/LookupAnything/Framework/Fields/ScheduleField.cs +++ b/LookupAnything/Framework/Fields/ScheduleField.cs @@ -5,74 +5,73 @@ using StardewValley; using StardewValley.Pathfinding; -namespace Pathoschild.Stardew.LookupAnything.Framework.Fields +namespace Pathoschild.Stardew.LookupAnything.Framework.Fields; + +/// A metadata field which shows an NPC's schedule. +/// The NPC's loaded schedule. +/// Provides utility methods for interacting with the game code. +internal class ScheduleField(Dictionary schedule, GameHelper gameHelper) : GenericField(I18n.Npc_Schedule(), GetText(schedule, gameHelper)) { - /// A metadata field which shows an NPC's schedule. - /// The NPC's loaded schedule. - /// Provides utility methods for interacting with the game code. - internal class ScheduleField(Dictionary schedule, GameHelper gameHelper) : GenericField(I18n.Npc_Schedule(), GetText(schedule, gameHelper)) + /// + public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth) { - /// - public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth) + float topOffset = 0; + + foreach (IFormattedText text in this.Value) { - float topOffset = 0; + topOffset += spriteBatch.DrawTextBlock(font, [text], new Vector2(position.X, position.Y + topOffset), wrapWidth).Y; + } - foreach (IFormattedText text in this.Value) - { - topOffset += spriteBatch.DrawTextBlock(font, [text], new Vector2(position.X, position.Y + topOffset), wrapWidth).Y; - } + return new Vector2(wrapWidth, topOffset); + } - return new Vector2(wrapWidth, topOffset); - } + /// Get the text to display. + /// An NPC's loaded schedule. + /// Provides utility methods for interacting with the game code. + private static IEnumerable GetText(Dictionary schedule, GameHelper gameHelper) + { + List formattedSchedule = FormatSchedule(schedule).ToList(); - /// Get the text to display. - /// An NPC's loaded schedule. - /// Provides utility methods for interacting with the game code. - private static IEnumerable GetText(Dictionary schedule, GameHelper gameHelper) + for (int i = 0; i < formattedSchedule.Count; i++) { - List formattedSchedule = FormatSchedule(schedule).ToList(); + (int time, SchedulePathDescription entry) = formattedSchedule[i]; - for (int i = 0; i < formattedSchedule.Count; i++) - { - (int time, SchedulePathDescription entry) = formattedSchedule[i]; + string timeString = formattedSchedule.Count == 1 ? I18n.Npc_Schedule_AllDay() : Game1.getTimeOfDayString(time); + string locationDisplayName = gameHelper.GetLocationDisplayName(entry.targetLocationName, Game1.getLocationFromName(entry.targetLocationName).GetData()); - string timeString = formattedSchedule.Count == 1 ? I18n.Npc_Schedule_AllDay() : Game1.getTimeOfDayString(time); - string locationDisplayName = gameHelper.GetLocationDisplayName(entry.targetLocationName, Game1.getLocationFromName(entry.targetLocationName).GetData()); + bool didCurrentEventStart = Game1.timeOfDay >= time; + bool didNextEventStart = i < formattedSchedule.Count - 1 && Game1.timeOfDay >= formattedSchedule[i + 1].Time; + Color textColor; - bool didCurrentEventStart = Game1.timeOfDay >= time; - bool didNextEventStart = i < formattedSchedule.Count - 1 && Game1.timeOfDay >= formattedSchedule[i + 1].Time; - Color textColor; + if (didCurrentEventStart) + textColor = didNextEventStart ? Color.Gray : Color.Green; + else + textColor = Color.Black; - if (didCurrentEventStart) - textColor = didNextEventStart ? Color.Gray : Color.Green; - else - textColor = Color.Black; - - yield return new FormattedText($"{timeString} - {locationDisplayName}", textColor); - } + yield return new FormattedText($"{timeString} - {locationDisplayName}", textColor); } + } - /// Returns a collection of schedule entries sorted by time. Consecutive entries with the same target location are omitted. - /// The schedule to format. - private static IEnumerable FormatSchedule(Dictionary schedule) - { - List sortedKeys = [.. schedule.Keys.OrderBy(key => key)]; - string prevTargetLocationName = string.Empty; + /// Returns a collection of schedule entries sorted by time. Consecutive entries with the same target location are omitted. + /// The schedule to format. + private static IEnumerable FormatSchedule(Dictionary schedule) + { + List sortedKeys = [.. schedule.Keys.OrderBy(key => key)]; + string prevTargetLocationName = string.Empty; - foreach (int time in sortedKeys) - { - // skip if the entry does not exist or the previous entry was for the same location - if (!schedule.TryGetValue(time, out SchedulePathDescription? entry) || entry.targetLocationName == prevTargetLocationName) - continue; + foreach (int time in sortedKeys) + { + // skip if the entry does not exist or the previous entry was for the same location + if (!schedule.TryGetValue(time, out SchedulePathDescription? entry) || entry.targetLocationName == prevTargetLocationName) + continue; - prevTargetLocationName = entry.targetLocationName; - yield return new ScheduleEntry(time, entry); - } + prevTargetLocationName = entry.targetLocationName; + yield return new ScheduleEntry(time, entry); } - - /// An entry in an NPC's schedule. - /// The time that the event starts. - /// A description of the event. - private record ScheduleEntry(int Time, SchedulePathDescription Description); } + + /// An entry in an NPC's schedule. + /// The time that the event starts. + /// A description of the event. + private record ScheduleEntry(int Time, SchedulePathDescription Description); }