Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lookup Anything] Add schedule to NPC lookup #1036

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions LookupAnything/DataParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,38 @@ public string GetLocationDisplayName(FishSpawnLocationData fishSpawnData)
return this.GetLocationDisplayName(fishSpawnData.LocationId, locationData, fishSpawnData.Area);
}

/// <summary>Get the translated display name for a location and optional fish area.</summary>
/// <param name="id">The location's internal name.</param>
/// <param name="data">The location data, if available.</param>
/// <param name="fishAreaId">The fish area ID within the location, if applicable.</param>
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;
}

/// <summary>Parse monster data.</summary>
/// <remarks>Reverse engineered from <see cref="StardewValley.Monsters.Monster.parseMonsterInfo"/>, <see cref="GameLocation.monsterDrop"/>, and the <see cref="Debris"/> constructor.</remarks>
public IEnumerable<MonsterData> GetMonsters()
Expand Down Expand Up @@ -652,38 +684,6 @@ from result in itemQueryResults
/*********
** Private methods
*********/
/// <summary>Get the translated display name for a location and optional fish area.</summary>
/// <param name="id">The location's internal name.</param>
/// <param name="data">The location data, if available.</param>
/// <param name="fishAreaId">The fish area ID within the location, if applicable.</param>
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;
}

/// <summary>Get the translated display name for a location.</summary>
/// <param name="id">The location's internal name.</param>
/// <param name="data">The location data, if available.</param>
Expand Down
77 changes: 77 additions & 0 deletions LookupAnything/Framework/Fields/ScheduleField.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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;

/// <summary>A metadata field which shows an NPC's schedule.</summary>
/// <param name="schedule">The NPC's loaded schedule.</param>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
internal class ScheduleField(Dictionary<int, SchedulePathDescription> schedule, GameHelper gameHelper) : GenericField(I18n.Npc_Schedule(), GetText(schedule, gameHelper))
{
/// <inheritdoc />
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);
}

/// <summary>Get the text to display.</summary>
/// <param name="schedule">An NPC's loaded schedule.</param>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
private static IEnumerable<IFormattedText> GetText(Dictionary<int, SchedulePathDescription> schedule, GameHelper gameHelper)
{
List<ScheduleEntry> 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());

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);
}
}

/// <summary>Returns a collection of schedule entries sorted by time. Consecutive entries with the same target location are omitted.</summary>
/// <param name="schedule">The schedule to format.</param>
private static IEnumerable<ScheduleEntry> FormatSchedule(Dictionary<int, SchedulePathDescription> schedule)
{
List<int> 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 new ScheduleEntry(time, entry);
}
}

/// <summary>An entry in an NPC's schedule.</summary>
/// <param name="Time">The time that the event starts.</param>
/// <param name="Description">A description of the event.</param>
private record ScheduleEntry(int Time, SchedulePathDescription Description);
}
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,12 @@ private IEnumerable<ICustomField> 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);
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions LookupAnything/GameHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -375,6 +376,15 @@ public string GetLocationDisplayName(FishSpawnLocationData fishSpawnData)
return this.DataParser.GetLocationDisplayName(fishSpawnData);
}

/// <summary>Get the translated display name for a location and optional fish area.</summary>
/// <param name="id">The location's internal name.</param>
/// <param name="data">The location data.</param>
/// <param name="fishAreaId">The fish area ID within the location, if applicable.</param>
public string GetLocationDisplayName(string id, LocationData data, string? fishAreaId = null)
{
return this.DataParser.GetLocationDisplayName(id, data, fishAreaId);
}

/// <summary>Parse monster data.</summary>
public IEnumerable<MonsterData> GetMonsterData()
{
Expand Down
6 changes: 6 additions & 0 deletions LookupAnything/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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


/*********
Expand Down
6 changes: 6 additions & 0 deletions LookupAnything/i18n/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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",


/*********
Expand Down
6 changes: 6 additions & 0 deletions LookupAnything/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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


/*********
Expand Down
6 changes: 6 additions & 0 deletions LookupAnything/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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


/*********
Expand Down
6 changes: 6 additions & 0 deletions LookupAnything/i18n/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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


/*********
Expand Down
6 changes: 6 additions & 0 deletions LookupAnything/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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


/*********
Expand Down
6 changes: 6 additions & 0 deletions LookupAnything/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "鉱山",
Expand Down Expand Up @@ -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
Expand All @@ -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


/*********
Expand Down
Loading