Skip to content

Commit

Permalink
Merge pull request #105 from Vivelin/feature/new-hints
Browse files Browse the repository at this point in the history
Added "Hey tracker, what's in <area>?"
  • Loading branch information
Vivelin authored Mar 31, 2022
2 parents a113b06 + 719153e commit fdc931b
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 13 deletions.
53 changes: 52 additions & 1 deletion src/Randomizer.SMZ3.Tracking/Configuration/HintsConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,60 @@ public class HintsConfig
/// <remarks>
/// <c>{0}</c> is a placeholder for the name of the location. <c>{1}</c>
/// is a placeholder for the text that would be displayed when using the
/// Book of Mudora. <c>{2}</c> is the name of the Book of Mudora, including "the".
/// Book of Mudora. <c>{2}</c> is the name of the Book of Mudora,
/// including "the".
/// </remarks>
public SchrodingersString BookHint { get; init; }
= new("If the item there was on the Master Sword Pedestal, it would say '{1}'.");

/// <summary>
/// Gets the phrases to respond with when asking for hints about an area
/// that was already cleared.
/// </summary>
/// <remarks>
/// <c>{0}</c> is a placeholder for the name of the area.
/// </remarks>
public SchrodingersString AreaAlreadyCleared { get; init; }
= new("You already got everything in {0}.");

/// <summary>
/// Gets the hint to give for an area that might have one or more useful
/// items.
/// </summary>
/// <remarks>
/// <c>{0}</c> is a placeholder for the name of the area.
/// </remarks>
public SchrodingersString AreaHasSomethingGood { get; init; }
= new("{0} might have something good.");

/// <summary>
/// Gets the hint to give for an area that only has junk items left.
/// </summary>
/// <remarks>
/// <c>{0}</c> is a placeholder for the name of the area.
/// </remarks>
public SchrodingersString AreaHasJunk { get; init; }
= new("{0} isn't worth your time.");

/// <summary>
/// Gets the hint to give for an area that only has junk items left, but
/// also has a crystal as reward for beating the boss.
/// </summary>
/// <remarks>
/// <c>{0}</c> is a placeholder for the name of the area.
/// </remarks>
public SchrodingersString AreaHasJunkAndCrystal { get; init; }
= new("{0} only has a crystal.");

/// <summary>
/// Gets the hint to give for an area whose worth is complicated, e.g.
/// when a dungeon has only junk and is not a crystal dungeon, but the
/// pendant might result in something good.
/// </summary>
/// <remarks>
/// <c>{0}</c> is a placeholder for the name of the area.
/// </remarks>
public SchrodingersString AreaWorthComplicated { get; init; }
= new("It's complicated.");
}
}
20 changes: 20 additions & 0 deletions src/Randomizer.SMZ3.Tracking/Configuration/ItemData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,21 @@ public bool TryGetTrackingResponse([NotNullWhen(true)] out SchrodingersString? r
return false;
}

/// <summary>
/// Determines whether the item is worth getting given the specified
/// configuration.
/// </summary>
/// <param name="config">The randomizer configuration.</param>
/// <returns>
/// <c>true</c> if the item is considered good; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// This method only considers the item's value on its own. Call <see
/// cref="Tracker.IsWorth(ItemData)"/> to include items that this item
/// might logically lead to.
/// </remarks>
public bool IsGood(Config config) => !IsJunk(config);

/// <summary>
/// Determines whether the item is junk given the specified
/// configuration.
Expand All @@ -297,6 +312,11 @@ public bool TryGetTrackingResponse([NotNullWhen(true)] out SchrodingersString? r
/// <returns>
/// <c>true</c> if the item is considered junk; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// This method only considers the item's value on its own. Call <see
/// cref="Tracker.IsWorth(ItemData)"/> to include items that this item
/// might logically lead to.
/// </remarks>
public bool IsJunk(Config config)
{
var junkCategories = config.Keysanity
Expand Down
10 changes: 10 additions & 0 deletions src/Randomizer.SMZ3.Tracking/Configuration/SpoilerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,15 @@ public class SpoilerConfig
/// </remarks>
public SchrodingersString ItemsAreAtOutOfLogicLocation { get; init; }
= new("There is {0} at {1} <break strength='weak'/> in {2}, but you cannot get it yet.");

/// <summary>
/// Gets the phrases that mention all the items in an area.
/// </summary>
/// <remarks>
/// <c>{0}</c> is a placeholder for the name of the room or region.
/// <c>{1}</c> is a placeholder for the names of the items left.
/// </remarks>
public SchrodingersString ItemsInArea { get; init; }
= new("{0} has {1}");
}
}
116 changes: 105 additions & 11 deletions src/Randomizer.SMZ3.Tracking/Tracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,20 @@ public void SetDungeonRequirement(DungeonInfo dungeon, Medallion? medallion = nu
return Items.FirstOrDefault(x => x.InternalItemType == type);
}

/// <summary>
/// Returns the first item that matches the item at the specified
/// location.
/// </summary>
/// <param name="location">The location with the item to find.</param>
/// <returns>
/// The item data for the item at the location, or <c>null</c> if no
/// item data is present for the item at the location.
/// </returns>
public ItemData? FindItem(Location location)
{
return Items.FirstOrDefault(x => x.InternalItemType == location.Item.Type);
}

/// <summary>
/// Gets the currently available items.
/// </summary>
Expand Down Expand Up @@ -1917,6 +1931,17 @@ internal void HandleInterruption()
}
}

internal void RestartIdleTimers()
{
foreach (var item in _idleTimers)
{
var timeout = Parse.AsTimeSpan(item.Key, s_random) ?? Timeout.InfiniteTimeSpan;
var timer = item.Value;

timer.Change(timeout, Timeout.InfiniteTimeSpan);
}
}

/// <summary>
/// Returns the possible names of the specified location.
/// </summary>
Expand All @@ -1938,6 +1963,86 @@ protected internal virtual SchrodingersString GetName(Location location)
protected internal virtual string GetName(ItemType item)
=> Items.SingleOrDefault(x => x.InternalItemType == item)?.NameWithArticle.ToString() ?? item.GetDescription();

/// <summary>
/// Determines whether or not the specified reward is worth getting.
/// </summary>
/// <param name="reward">The dungeon reward.</param>
/// <returns>
/// <see langword="true"/> if the reward leads to something good;
/// otherwise, <see langword="false"/>.
/// </returns>
protected internal bool IsWorth(Reward reward)
{
var sahasrahlaItem = FindItemByType(World.LightWorldNorthEast.SahasrahlasHideout.Sahasrahla.Item.Type);
if (sahasrahlaItem != null && reward == Reward.PendantGreen)
{
_logger.LogDebug("{Reward} leads to {Item}...", reward, sahasrahlaItem);
if (IsWorth(sahasrahlaItem))
{
_logger.LogDebug("{Reward} leads to {Item}, which is worth it", reward, sahasrahlaItem);
return true;
}
_logger.LogDebug("{Reward} leads to {Item}, which is junk", reward, sahasrahlaItem);
}

var pedItem = FindItemByType(World.LightWorldNorthWest.MasterSwordPedestal.Item.Type);
if (pedItem != null && (reward == Reward.PendantGreen || reward == Reward.PendantNonGreen))
{
_logger.LogDebug("{Reward} leads to {Item}...", reward, pedItem);
if (IsWorth(pedItem))
{
_logger.LogDebug("{Reward} leads to {Item}, which is worth it", reward, pedItem);
return true;
}
_logger.LogDebug("{Reward} leads to {Item}, which is junk", reward, pedItem);
}

return false;
}

/// <summary>
/// Determines whether or not the specified item is worth getting.
/// </summary>
/// <param name="item">The item whose worth to consider.</param>
/// <returns>
/// <see langword="true"/> is the item is worth getting or leads to
/// another item that is worth getting; otherwise, <see
/// langword="false"/>.
/// </returns>
protected internal bool IsWorth(ItemData item)
{
var leads = new Dictionary<ItemType, Location[]>()
{
[ItemType.Mushroom] = new[] { World.LightWorldNorthEast.MushroomItem },
[ItemType.Powder] = new[] { World.LightWorldNorthWest.MagicBat },
[ItemType.Book] = new[]
{
World.LightWorldDeathMountainWest.EtherTablet,
World.LightWorldSouth.BombosTablet
}
};

if (leads.TryGetValue(item.InternalItemType, out var leadsToLocation))
{
foreach (var location in leadsToLocation)
{
var reward = FindItem(location);
if (reward != null)
{
_logger.LogDebug("{Item} leads to {OtherItem}...", item, reward);
if (IsWorth(reward))
{
_logger.LogDebug("{Item} leads to {OtherItem}, which is worth it", item, reward);
return true;
}
_logger.LogDebug("{Item} leads to {OtherItem}, which is junk", item, reward);
}
}
}

return item.IsGood(World.Config);
}

/// <summary>
/// Adds an action to be invoked to undo the last operation.
/// </summary>
Expand Down Expand Up @@ -2175,17 +2280,6 @@ private void GiveLocationComment(ItemData item, Location location, bool isTracki
return dungeon;
}

internal void RestartIdleTimers()
{
foreach (var item in _idleTimers)
{
var timeout = Parse.AsTimeSpan(item.Key, s_random) ?? Timeout.InfiniteTimeSpan;
var timer = item.Value;

timer.Change(timeout, Timeout.InfiniteTimeSpan);
}
}

private void IdleTimerElapsed(object? state)
{
var key = (string)state!;
Expand Down
94 changes: 94 additions & 0 deletions src/Randomizer.SMZ3.Tracking/VoiceCommands/SpoilerModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography.X509Certificates;

using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -78,6 +79,12 @@ public SpoilerModule(Tracker tracker, ILogger<SpoilerModule> logger)
{
GiveProgressionHint();
});

AddCommand("Give area hint", GetLocationUsefulnessHintRule(), (tracker, result) =>
{
var area = GetAreaFromResult(tracker, result);
GiveAreaHint(area);
});
}

/// <summary>
Expand Down Expand Up @@ -119,6 +126,64 @@ public void GiveProgressionHint()
Tracker.Say(x => x.Hints.NoApplicableHints);
}

/// <summary>
/// Gives a hint or spoiler about useful items in an area.
/// </summary>
/// <param name="area">The area to give hints about.</param>
public void GiveAreaHint(IHasLocations area)
{
if (!HintsEnabled && !SpoilersEnabled)
{
Tracker.Say(x => x.Hints.PromptEnableItemHints);
return;
}

var locations = area.Locations
.Where(x => !x.Cleared)
.ToImmutableList();
if (locations.Count == 0)
{
Tracker.Say(x => x.Hints.AreaAlreadyCleared, area.GetName());
return;
}

var items = locations
.Select(x => Tracker.FindItemByType(x.Item.Type))
.NonNull();
if (SpoilersEnabled)
{
var itemNames = NaturalLanguage.Join(items, Tracker.World.Config);
Tracker.Say(x => x.Spoilers.ItemsInArea, area.GetName(), itemNames);
}
else if (HintsEnabled)
{
if (items.Any(x => !x.IsJunk(Tracker.World.Config)))
{
Tracker.Say(x => x.Hints.AreaHasSomethingGood, area.GetName());
}
else if (area is IHasReward region)
{
if (region.Reward == Reward.CrystalBlue
|| region.Reward == Reward.CrystalRed)
{
Tracker.Say(x => x.Hints.AreaHasJunkAndCrystal, area.GetName());
}
else if (Tracker.IsWorth(region.Reward))
{
Tracker.Say(x => x.Hints.AreaWorthComplicated, area.GetName());
}
else
{
Tracker.Say(x => x.Hints.AreaHasJunk, area.GetName());
}
}
else
{
Tracker.Say(x => x.Hints.AreaHasJunk, area.GetName());
}
}
}

/// <summary>
/// Gives a hint or spoiler about the location of an item.
/// </summary>
Expand Down Expand Up @@ -708,5 +773,34 @@ private GrammarBuilder GetProgressionHintRule()
"where should I go?",
"what should I do?");
}

private GrammarBuilder GetLocationUsefulnessHintRule()
{
var regionNames = GetRegionNames();
var regionGrammar = new GrammarBuilder()
.Append("Hey tracker, ")
.OneOf("is there anything useful",
"is there anything I need",
"is there anything good",
"is there anything",
"what's left",
"what's")
.OneOf("in", "at")
.Append(RegionKey, regionNames);

var roomNames = GetRoomNames();
var roomGrammar = new GrammarBuilder()
.Append("Hey tracker, ")
.OneOf("is there anything useful",
"is there anything I need",
"is there anything good",
"is there anything",
"what's left",
"what's")
.OneOf("in", "at")
.Append(RoomKey, roomNames);

return GrammarBuilder.Combine(regionGrammar, roomGrammar);
}
}
}
Loading

0 comments on commit fdc931b

Please sign in to comment.