/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="taste">How much the target villager likes this item.</param> /// <param name="villagerName">The name of the target villager.</param> /// <param name="refID">The item parent sprite index (if positive) or category (if negative).</param> /// <param name="isUniversal">Whether this gift taste applies to all villagers unless otherwise excepted.</param> public GiftTasteEntry(GiftTaste taste, string villagerName, int refID, bool isUniversal = false) { this.Taste = taste; this.VillagerName = villagerName; this.RefID = refID; this.IsUniversal = isUniversal; }
/// <summary>Get the raw gift tastes from the underlying data.</summary> /// <param name="objects">The game's object data.</param> /// <remarks>Reverse engineered from <c>Data\NPCGiftTastes</c> and <see cref="StardewValley.NPC.getGiftTasteForThisItem"/>.</remarks> public IEnumerable <GiftTasteModel> GetGiftTastes(ObjectModel[] objects) { // extract raw values var tastes = new List <GiftTasteModel>(); { // define data schema var universal = new Dictionary <string, GiftTaste> { ["Universal_Love"] = GiftTaste.Love, ["Universal_Like"] = GiftTaste.Like, ["Universal_Neutral"] = GiftTaste.Neutral, ["Universal_Dislike"] = GiftTaste.Dislike, ["Universal_Hate"] = GiftTaste.Hate }; var personalMetadataKeys = new Dictionary <int, GiftTaste> { // metadata is paired: odd values contain a list of item references, even values contain the reaction dialogue [1] = GiftTaste.Love, [3] = GiftTaste.Like, [5] = GiftTaste.Dislike, [7] = GiftTaste.Hate, [9] = GiftTaste.Neutral }; // read data IDictionary <string, string> data = Game1.NPCGiftTastes; foreach (string villager in data.Keys) { string tasteStr = data[villager]; if (universal.ContainsKey(villager)) { GiftTaste taste = universal[villager]; tastes.AddRange( from refID in tasteStr.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) select new GiftTasteModel(taste, "*", int.Parse(refID), isUniversal: true) ); } else { string[] personalData = tasteStr.Split('/'); foreach (KeyValuePair <int, GiftTaste> taste in personalMetadataKeys) { tastes.AddRange( from refID in personalData[taste.Key].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) select new GiftTasteModel(taste.Value, villager, int.Parse(refID)) ); } } } } // get sanitised data HashSet <int> validItemIDs = new HashSet <int>(objects.Select(p => p.ParentSpriteIndex)); HashSet <int> validCategories = new HashSet <int>(objects.Where(p => p.Category != 0).Select(p => p.Category)); return(tastes .Where(model => validCategories.Contains(model.RefID) || validItemIDs.Contains(model.RefID))); // ignore invalid entries }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="taste">How much the target villager likes this item.</param> /// <param name="villager">The name of the target villager.</param> /// <param name="refID">The item parent sprite index (if positive) or category (if negative).</param> /// <param name="isUniversal">Whether this gift taste applies to all villagers unless otherwise excepted.</param> public GiftTasteModel(GiftTaste taste, string villager, int refID, bool isUniversal = false) { this.Taste = taste; this.Villager = villager; this.RefID = refID; this.IsUniversal = isUniversal; }
/// <summary>Adds an item for an npc to the database.</summary> public virtual bool AddGift(string npcName, int itemId, GiftTaste taste) { if (taste == GiftTaste.MAX) { return(false); } bool check = true; if (!Database.Entries.ContainsKey(npcName)) { Database.Entries.Add(npcName, new CharacterTasteModel()); check = false; } if (!check || !ContainsGift(npcName, itemId, taste)) { Utils.DebugLog($"Adding {itemId} to {npcName}'s {taste} tastes."); Database.Entries[npcName].Add(taste, new GiftModel() { ItemId = itemId }); DatabaseChanged(); return(true); } return(false); }
public static int[] GetItemsForTaste(string npcName, GiftTaste taste) { Debug.Assert(taste != GiftTaste.MAX); if (!Game1.NPCGiftTastes.ContainsKey(npcName)) { return(new int[] { }); } var giftTaste = Game1.NPCGiftTastes[npcName]; if (UniversalTastes.ContainsKey(npcName)) { // Universal tastes are parsed differently return(Utils.StringToIntArray(giftTaste.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))); } string[] giftTastes = giftTaste.Split('/'); if (giftTastes.Length == 0) { return(new int[] { }); } // See http://stardewvalleywiki.com/Modding:Gift_taste_data int tasteIndex = (int)taste + 1; // Enum value is the even number which is the dialogue, odd is the list of item refs. if (giftTastes[tasteIndex].Length > 0) { return(Utils.StringToIntArray(giftTastes[tasteIndex].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))); } return(new int[] { }); }
/// <summary>Adds a range of items for an npc to the database.</summary> public virtual bool AddGifts(string npcName, GiftTaste taste, int[] itemIds) { if (taste == GiftTaste.MAX) { return(false); } if (!Database.Entries.ContainsKey(npcName)) { Database.Entries.Add(npcName, new CharacterTasteModel()); } // Add only the gifts that are not already in the DB. var unique = itemIds.Where(id => !ContainsGift(npcName, id, taste)).Select(id => id); if (unique.Count() > 0) { Database.Entries[npcName].AddRange(taste, itemIds.Select(id => new GiftModel() { ItemId = id })); DatabaseChanged(); return(true); } return(false); }
/// <summary>Returns if the database has an item for a particular NPC stored.</summary> public bool ContainsGift(string npcName, int itemId, GiftTaste taste) { if (taste == GiftTaste.MAX) { return(false); } return(Database.Entries[npcName].Contains(taste, itemId)); }
public override bool AddGifts(string npcName, GiftTaste taste, int[] itemIds) { if (base.AddGifts(npcName, taste, itemIds)) { Write(); return(true); } return(false); }
public override bool AddGift(string npcName, int itemId, GiftTaste taste) { if (base.AddGift(npcName, itemId, taste)) { Write(); return(true); } return(false); }
/********* ** Private methods *********/ /// <summary>Get the text to display.</summary> /// <param name="giftTastes">NPCs by how much they like receiving this item.</param> /// <param name="showTaste">The gift taste to show.</param> private static string GetText(IDictionary <GiftTaste, string[]> giftTastes, GiftTaste showTaste) { if (!giftTastes.ContainsKey(showTaste)) { return(null); } string[] names = giftTastes[showTaste].OrderBy(p => p).ToArray(); return(string.Join(", ", names)); }
public int GiftTasteRating(NpcHandler npcHandler) { if (Item == null) { return(0); } GiftTaste giftTaste = new GiftTaste(Item, npcHandler); return(giftTaste.Rating); }
public virtual IEnumerable <int> GetGifts(string npcName, GiftTaste taste, bool includeUniversal) { IEnumerable <int> gifts = Database.GetGiftsForTaste(npcName, taste); if (includeUniversal) { // Individual NPC tastes may conflict with the universal ones. return(GetUniversalGifts(npcName, taste).Concat(gifts)); } return(gifts); }
/// <summary>Returns all the gifts of the given taste in the database for that npc.</summary> public int[] GetGiftsForTaste(string npcName, GiftTaste taste) { if (Database.Entries.ContainsKey(npcName)) { var entryForTaste = Database.Entries[npcName][taste]; if (entryForTaste != null) { return(entryForTaste.Select(model => model.ItemId).ToArray()); } } return(new int[] { }); }
/// <summary>Get how much each NPC likes watching this week's movie.</summary> public IEnumerable <KeyValuePair <NPC, GiftTaste> > GetMovieTastes() { foreach (NPC npc in this.GetAllCharacters()) { if (!this.IsSocialVillager(npc)) { continue; } GiftTaste taste = (GiftTaste)Enum.Parse(typeof(GiftTaste), MovieTheater.GetResponseForMovie(npc), ignoreCase: true); yield return(new KeyValuePair <NPC, GiftTaste>(npc, taste)); } }
public override IEnumerable <int> GetGifts(string npcName, GiftTaste taste, bool includeUniversal) { // Universal gifts are stored with the regular ones in the DB so we need to remove them if they shouldn't be included. var gifts = base.GetGifts(npcName, taste, includeUniversal); if (!includeUniversal) { // Filter out any that are also in the universal table. // Note that this probably won't work correctly for categories, but we're not bothering with those for now. var universal = Utils.GetItemsForTaste(Utils.UniversalTasteNames[taste], taste); return(gifts.Except(universal)); } return(gifts); }
public void Add(string name, Item it) { NPC character = Game1.getCharacterFromName(name); if (null == character) { return; } // look up npc's gift taste for this item // reverse-engineering Game1.NPCGiftTastes is unnecessary and I will not be attempting it GiftTaste gt = (GiftTaste)character.getGiftTasteForThisItem(it); // if NPC is not listed or this gift is already known if (!Data.ContainsKey(name) || Data[name][gt].Contains(it.Name)) { return; } Data[name][gt].Add(it.Name); Log.Out(name + " " + GiftTasteHelper(gt) + " " + it.Name); }
static string GiftTasteHelper(GiftTaste gt) { switch (gt) { case GiftTaste.eGiftTaste_Love: return("loves"); case GiftTaste.eGiftTaste_Like: return("likes"); case GiftTaste.eGiftTaste_Dislike: return("dislikes"); case GiftTaste.eGiftTaste_Hate: return("hates"); case GiftTaste.eGiftTaste_Neutral: return("neutral"); default: return("unknown"); } }
bool DisplayCategory(GiftTaste category) { switch (category) { case GiftTaste.eGiftTaste_Love: return(true); case GiftTaste.eGiftTaste_Like: return(ModConfig.ShowLikes); case GiftTaste.eGiftTaste_Dislike: return(ModConfig.ShowDislikes); case GiftTaste.eGiftTaste_Hate: return(ModConfig.ShowHates); case GiftTaste.eGiftTaste_Neutral: return(ModConfig.ShowNeutral); default: return(false); } }
public virtual IEnumerable <int> GetUniversalGifts(string npcName, GiftTaste taste) { return(Database.GetGiftsForTaste(Utils.UniversalTasteNames[taste], taste) .Where(itemId => Utils.GetTasteForGift(npcName, itemId) == taste)); }
// See http://stardewvalleywiki.com/Modding:Gift_taste_data public static GiftTaste GetTasteForGift(string npcName, int itemId) { if (!Game1.NPCGiftTastes.ContainsKey(npcName)) { return(GiftTaste.MAX); } if (!Game1.objectInformation.ContainsKey(itemId)) { // Item is likely a category return(GiftTaste.MAX); } GiftTaste taste = GiftTaste.Neutral; string[] giftTastes = Game1.NPCGiftTastes[npcName].Split('/'); Debug.Assert(giftTastes.Length > 0); if (giftTastes.Length == 0) { return(taste); } var itemData = ItemData.MakeItem(itemId); // Part I: universal taste by category GiftTaste UniversalTasteForCategory(int cat) { foreach (var pair in UniversalTastes) { if (GetItemsForTaste(pair.Key, pair.Value).Contains(cat)) { return(pair.Value); } } return(GiftTaste.Neutral); } if (itemData.Category.Valid) { taste = UniversalTasteForCategory(itemData.Category.ID); } // Part II: universal taste by item ID GiftTaste GetUniversalTaste(int id) { foreach (var pair in UniversalTastes) { if (GetItemsForTaste(pair.Key, pair.Value).Contains(id)) { return(pair.Value); } } return(GiftTaste.MAX); } var universalTaste = GetUniversalTaste(itemData.ID); bool hasUniversalId = universalTaste != GiftTaste.MAX; bool hasUniversalNeutralId = universalTaste == GiftTaste.Neutral; taste = universalTaste != GiftTaste.MAX ? universalTaste : taste; // Part III: override neutral if it's from universal category if (taste == GiftTaste.Neutral && !hasUniversalNeutralId) { if (itemData.Edible && itemData.TastesBad) { taste = GiftTaste.Hate; } else if (itemData.Price < 20) { taste = GiftTaste.Dislike; } else if (itemData.Category.Name == "Arch") { taste = npcName == "Penny" ? GiftTaste.Like : GiftTaste.Dislike; } } // part IV: sometimes override with personal tastes var personalMetadataKeys = new Dictionary <int, GiftTaste> { // metadata is paired: odd values contain a list of item references, even values contain the reaction dialogue [1] = GiftTaste.Love, [7] = GiftTaste.Hate, // Hate has precedence [3] = GiftTaste.Like, [5] = GiftTaste.Dislike, [9] = GiftTaste.Neutral }; foreach (var pair in personalMetadataKeys) { if (giftTastes[pair.Key].Length > 0) { var items = Utils.StringToIntArray(giftTastes[pair.Key].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); bool hasTasteForItemOrCategory = items.Contains(itemData.ID) || (itemData.Category.Valid && items.Contains(itemData.Category.ID)); bool noCategoryOrNoTasteForCategory = !itemData.Category.Valid || !items.Contains(itemData.Category.ID); if (hasTasteForItemOrCategory && (noCategoryOrNoTasteForCategory || !hasUniversalId)) { return(pair.Value); } } } return(taste); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="gameHelper">Provides utility methods for interacting with the game code.</param> /// <param name="label">A short field label.</param> /// <param name="giftTastes">The items by how much this NPC likes receiving them.</param> /// <param name="showTaste">The gift taste to show.</param> /// <param name="onlyRevealed">Only show gift tastes the player has discovered for themselves.</param> /// <param name="highlightUnrevealed">Whether to highlight items which haven't been revealed in the NPC profile yet.</param> public CharacterGiftTastesField(GameHelper gameHelper, string label, IDictionary <GiftTaste, GiftTasteModel[]> giftTastes, GiftTaste showTaste, bool onlyRevealed, bool highlightUnrevealed) : base(label, CharacterGiftTastesField.GetText(gameHelper, giftTastes, showTaste, onlyRevealed, highlightUnrevealed)) { }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="label">A short field label.</param> /// <param name="giftTastes">NPCs by how much they like receiving this item.</param> /// <param name="showTaste">The gift taste to show.</param> public ItemGiftTastesField(string label, IDictionary <GiftTaste, string[]> giftTastes, GiftTaste showTaste) : base(label, ItemGiftTastesField.GetText(giftTastes, showTaste)) { }
/// <summary>Get the text to display.</summary> /// <param name="giftTastes">The items by how much this NPC likes receiving them.</param> /// <param name="showTaste">The gift taste to show.</param> /// <param name="onlyRevealed">Only show gift tastes the player has discovered for themselves.</param> /// <param name="highlightUnrevealed">Whether to highlight items which haven't been revealed in the NPC profile yet.</param> /// <param name="ownedItemsCache">A lookup cache for owned items, as created by <see cref="GetOwnedItemsCache"/>.</param> private static IEnumerable <IFormattedText> GetText(IDictionary <GiftTaste, GiftTasteModel[]> giftTastes, GiftTaste showTaste, bool onlyRevealed, bool highlightUnrevealed, IDictionary <string, bool> ownedItemsCache) { if (!giftTastes.ContainsKey(showTaste)) { yield break; } // get data var items = ( from entry in giftTastes[showTaste] let item = entry.Item let inInventory = ownedItemsCache.TryGetValue(CharacterGiftTastesField.GetOwnedItemKey(item), out bool rawVal) ? rawVal : null as bool? let isOwned = inInventory != null where !onlyRevealed || entry.IsRevealed orderby inInventory ?? false descending, isOwned descending, item.DisplayName select new { Item = item, IsInventory = inInventory ?? false, IsOwned = isOwned, isRevealed = entry.IsRevealed } ) .ToArray(); int unrevealed = onlyRevealed ? giftTastes[showTaste].Count(p => !p.IsRevealed) : 0; // generate text if (items.Any()) { for (int i = 0, last = items.Length - 1; i <= last; i++) { var entry = items[i]; string text = i != last ? entry.Item.DisplayName + ", " : entry.Item.DisplayName; bool bold = highlightUnrevealed && !entry.isRevealed; if (entry.IsInventory) { yield return(new FormattedText(text, Color.Green, bold)); } else if (entry.IsOwned) { yield return(new FormattedText(text, Color.Black, bold)); } else { yield return(new FormattedText(text, Color.Gray, bold)); } } if (unrevealed > 0) { yield return(new FormattedText(I18n.Npc_UndiscoveredGiftTasteAppended(count: unrevealed), Color.Gray)); } } else { yield return(new FormattedText(I18n.Npc_UndiscoveredGiftTaste(count: unrevealed), Color.Gray)); } }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="villager">The target villager.</param> /// <param name="item">A sample of the item.</param> /// <param name="taste">How much the target villager likes this item.</param> public GiftTasteModel(NPC villager, Item item, GiftTaste taste) { this.Villager = villager; this.Item = item; this.Taste = taste; }
/// <summary>Get a list of gift tastes for an NPC.</summary> /// <param name="label">The field label.</param> /// <param name="giftTastes">The gift taste data.</param> /// <param name="taste">The gift taste to display.</param> private ICustomField GetGiftTasteField(string label, IDictionary <GiftTaste, GiftTasteModel[]> giftTastes, GiftTaste taste) { return(new CharacterGiftTastesField(this.GameHelper, label, giftTastes, taste, onlyRevealed: this.ProgressionMode, highlightUnrevealed: this.HighlightUnrevealedGiftTastes)); }
public override IEnumerable <int> GetUniversalGifts(string npcName, GiftTaste taste) { var universal = Utils.GetItemsForTaste(Utils.UniversalTasteNames[taste], taste); return(Database.GetGiftsForTaste(npcName, taste).Intersect(universal)); }
/// <summary>Get the data to display for this subject.</summary> public override IEnumerable <ICustomField> GetData() { // get data Item item = this.Target; SObject?obj = item as SObject; bool isCrop = this.FromCrop != null; bool isSeed = this.SeedForCrop != null; bool isDeadCrop = this.FromCrop?.dead.Value == true; bool canSell = obj?.canBeShipped() == true || this.Metadata.Shops.Any(shop => shop.BuysCategories.Contains(item.Category)); bool isMovieTicket = obj?.ParentSheetIndex == 809 && !obj.bigCraftable.Value; // get overrides bool showInventoryFields = !this.IsSpawnedStoneNode(); { ObjectData?objData = this.Metadata.GetObject(item, this.Context); if (objData != null) { this.Name = objData.NameKey != null?I18n.GetByKey(objData.NameKey) : this.Name; this.Description = objData.DescriptionKey != null?I18n.GetByKey(objData.DescriptionKey) : this.Description; this.Type = objData.TypeKey != null?I18n.GetByKey(objData.TypeKey) : this.Type; showInventoryFields = objData.ShowInventoryFields ?? showInventoryFields; } } // don't show data for dead crop if (isDeadCrop) { yield return(new GenericField(I18n.Crop_Summary(), I18n.Crop_Summary_Dead())); yield break; } // crop fields foreach (ICustomField field in this.GetCropFields(this.FromDirt, this.FromCrop ?? this.SeedForCrop, isSeed)) { yield return(field); } // indoor pot crop if (obj is IndoorPot pot) { Crop?potCrop = pot.hoeDirt.Value.crop; Bush?potBush = pot.bush.Value; if (potCrop != null) { Item drop = this.GameHelper.GetObjectBySpriteIndex(potCrop.indexOfHarvest.Value); yield return(new LinkField(I18n.Item_Contents(), drop.DisplayName, () => this.GetCropSubject(potCrop, ObjectContext.World, pot.hoeDirt.Value))); } if (potBush != null) { ISubject?subject = this.Codex.GetByEntity(potBush, this.Location ?? potBush.currentLocation); if (subject != null) { yield return(new LinkField(I18n.Item_Contents(), subject.Name, () => subject)); } } } // machine output foreach (ICustomField field in this.GetMachineOutputFields(obj)) { yield return(field); } // music blocks if (obj?.Name == "Flute Block") { yield return(new GenericField(I18n.Item_MusicBlock_Pitch(), I18n.Generic_Ratio(value: obj.preservedParentSheetIndex.Value, max: 2300))); } else if (obj?.Name == "Drum Block") { yield return(new GenericField(I18n.Item_MusicBlock_DrumType(), I18n.Generic_Ratio(value: obj.preservedParentSheetIndex.Value, max: 6))); } // item if (showInventoryFields) { // needed for foreach (ICustomField field in this.GetNeededForFields(obj)) { yield return(field); } // sale data if (canSell && !isCrop) { // sale price string?saleValueSummary = GenericField.GetSaleValueString(this.GetSaleValue(item, this.KnownQuality), item.Stack); yield return(new GenericField(I18n.Item_SellsFor(), saleValueSummary)); // sell to List <string> buyers = new(); if (obj?.canBeShipped() == true) { buyers.Add(I18n.Item_SellsTo_ShippingBox()); } buyers.AddRange( from shop in this.Metadata.Shops where shop.BuysCategories.Contains(item.Category) let name = I18n.GetByKey(shop.DisplayKey).ToString() orderby name select name ); yield return(new GenericField(I18n.Item_SellsTo(), string.Join(", ", buyers))); } // clothing if (item is Clothing clothing) { yield return(new GenericField(I18n.Item_CanBeDyed(), this.Stringify(clothing.dyeable.Value))); } // gift tastes if (!isMovieTicket) { IDictionary <GiftTaste, GiftTasteModel[]> giftTastes = this.GetGiftTastes(item); yield return(new ItemGiftTastesField(I18n.Item_LovesThis(), giftTastes, GiftTaste.Love, onlyRevealed: this.ProgressionMode, highlightUnrevealed: this.HighlightUnrevealedGiftTastes)); yield return(new ItemGiftTastesField(I18n.Item_LikesThis(), giftTastes, GiftTaste.Like, onlyRevealed: this.ProgressionMode, highlightUnrevealed: this.HighlightUnrevealedGiftTastes)); if (this.ProgressionMode || this.HighlightUnrevealedGiftTastes || this.ShowAllGiftTastes) { yield return(new ItemGiftTastesField(I18n.Item_NeutralAboutThis(), giftTastes, GiftTaste.Neutral, onlyRevealed: this.ProgressionMode, highlightUnrevealed: this.HighlightUnrevealedGiftTastes)); yield return(new ItemGiftTastesField(I18n.Item_DislikesThis(), giftTastes, GiftTaste.Dislike, onlyRevealed: this.ProgressionMode, highlightUnrevealed: this.HighlightUnrevealedGiftTastes)); yield return(new ItemGiftTastesField(I18n.Item_HatesThis(), giftTastes, GiftTaste.Hate, onlyRevealed: this.ProgressionMode, highlightUnrevealed: this.HighlightUnrevealedGiftTastes)); } } } // recipes if (showInventoryFields) { RecipeModel[] recipes = // recipes that take this item as ingredient this.GameHelper.GetRecipesForIngredient(this.DisplayItem) .Concat(this.GameHelper.GetRecipesForIngredient(item)) // recipes which produce this item .Concat(this.GameHelper.GetRecipesForOutput(this.DisplayItem)) .Concat(this.GameHelper.GetRecipesForOutput(item)) // recipes for a machine .Concat(this.GameHelper.GetRecipesForMachine(this.DisplayItem as SObject)) .Concat(this.GameHelper.GetRecipesForMachine(item as SObject)) .ToArray(); if (recipes.Any()) { yield return(new ItemRecipesField(this.GameHelper, I18n.Item_Recipes(), item, recipes.ToArray())); } } // fish spawn rules if (item.Category == SObject.FishCategory) { yield return(new FishSpawnRulesField(this.GameHelper, I18n.Item_FishSpawnRules(), item.ParentSheetIndex)); } // fish pond data // derived from FishPond::doAction and FishPond::isLegalFishForPonds if (!item.HasContextTag("fish_legendary") && (item.Category == SObject.FishCategory || Utility.IsNormalObjectAtParentSheetIndex(item, 393 /*coral*/) || Utility.IsNormalObjectAtParentSheetIndex(item, 397 /*sea urchin*/))) { foreach (FishPondData fishPondData in Game1.content.Load <List <FishPondData> >("Data\\FishPondData")) { if (!fishPondData.RequiredTags.All(item.HasContextTag)) { continue; } int minChanceOfAnyDrop = (int)Math.Round(Utility.Lerp(0.15f, 0.95f, 1 / 10f) * 100); int maxChanceOfAnyDrop = (int)Math.Round(Utility.Lerp(0.15f, 0.95f, FishPond.MAXIMUM_OCCUPANCY / 10f) * 100); string preface = I18n.Building_FishPond_Drops_Preface(chance: I18n.Generic_Range(min: minChanceOfAnyDrop, max: maxChanceOfAnyDrop)); yield return(new FishPondDropsField(this.GameHelper, I18n.Item_FishPondDrops(), -1, fishPondData, preface)); break; } } // fence if (item is Fence fence) { string healthLabel = I18n.Item_FenceHealth(); // health if (Game1.getFarm().isBuildingConstructed(Constant.BuildingNames.GoldClock)) { yield return(new GenericField(healthLabel, I18n.Item_FenceHealth_GoldClock())); } else { float maxHealth = fence.isGate.Value ? fence.maxHealth.Value * 2 : fence.maxHealth.Value; float health = fence.health.Value / maxHealth; double daysLeft = Math.Round(fence.health.Value * this.Constants.FenceDecayRate / 60 / 24); double percent = Math.Round(health * 100); yield return(new PercentageBarField(healthLabel, (int)fence.health.Value, (int)maxHealth, Color.Green, Color.Red, I18n.Item_FenceHealth_Summary(percent: (int)percent, count: (int)daysLeft))); } } // movie ticket if (isMovieTicket) { MovieData movie = MovieTheater.GetMovieForDate(Game1.Date); if (movie == null) { yield return(new GenericField(I18n.Item_MovieTicket_MovieThisWeek(), I18n.Item_MovieTicket_MovieThisWeek_None())); } else { // movie this week yield return(new GenericField(I18n.Item_MovieTicket_MovieThisWeek(), new IFormattedText[] { new FormattedText(movie.Title, bold: true), new FormattedText(Environment.NewLine), new FormattedText(movie.Description) })); // movie tastes const GiftTaste rejectKey = (GiftTaste)(-1); IDictionary <GiftTaste, string[]> tastes = this.GameHelper.GetMovieTastes() .GroupBy(entry => entry.Value ?? rejectKey) .ToDictionary(group => group.Key, group => group.Select(p => p.Key.Name).OrderBy(p => p).ToArray()); yield return(new MovieTastesField(I18n.Item_MovieTicket_LovesMovie(), tastes, GiftTaste.Love)); yield return(new MovieTastesField(I18n.Item_MovieTicket_LikesMovie(), tastes, GiftTaste.Like)); yield return(new MovieTastesField(I18n.Item_MovieTicket_DislikesMovie(), tastes, GiftTaste.Dislike)); yield return(new MovieTastesField(I18n.Item_MovieTicket_RejectsMovie(), tastes, rejectKey)); } } // dyes if (showInventoryFields) { yield return(new ColorField(I18n.Item_ProducesDye(), item)); } // owned and times cooked/crafted if (showInventoryFields && !isCrop) { // owned yield return(new GenericField(I18n.Item_NumberOwned(), I18n.Item_NumberOwned_Summary(count: this.GameHelper.CountOwnedItems(item)))); // times crafted RecipeModel[] recipes = this.GameHelper .GetRecipes() .Where(recipe => recipe.OutputItemIndex == this.Target.ParentSheetIndex && recipe.OutputItemType == this.Target.GetItemType()) .ToArray(); if (recipes.Any()) { string label = recipes.First().Type == RecipeType.Cooking ? I18n.Item_NumberCooked() : I18n.Item_NumberCrafted(); int timesCrafted = recipes.Sum(recipe => recipe.GetTimesCrafted(Game1.player)); if (timesCrafted >= 0) // negative value means not available for this recipe type { yield return(new GenericField(label, I18n.Item_NumberCrafted_Summary(count: timesCrafted))); } } } // see also crop bool seeAlsoCrop = isSeed && item.ParentSheetIndex != this.SeedForCrop !.indexOfHarvest.Value && // skip seeds which produce themselves (e.g. coffee beans) item.ParentSheetIndex is not(495 or 496 or 497) && // skip random seasonal seeds item.ParentSheetIndex != 770; // skip mixed seeds if (seeAlsoCrop) { Item drop = this.GameHelper.GetObjectBySpriteIndex(this.SeedForCrop !.indexOfHarvest.Value); yield return(new LinkField(I18n.Item_SeeAlso(), drop.DisplayName, () => this.GetCropSubject(this.SeedForCrop, ObjectContext.Inventory, null))); } }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="label">A short field label.</param> /// <param name="giftTastes">The items by how much this NPC likes receiving them.</param> /// <param name="showTaste">The gift taste to show.</param> /// <param name="onlyRevealed">Only show gift tastes the player has discovered for themselves.</param> /// <param name="highlightUnrevealed">Whether to highlight items which haven't been revealed in the NPC profile yet.</param> /// <param name="ownedItemsCache">A lookup cache for owned items, as created by <see cref="GetOwnedItemsCache"/>.</param> public CharacterGiftTastesField(string label, IDictionary <GiftTaste, GiftTasteModel[]> giftTastes, GiftTaste showTaste, bool onlyRevealed, bool highlightUnrevealed, IDictionary <string, bool> ownedItemsCache) : base(label, CharacterGiftTastesField.GetText(giftTastes, showTaste, onlyRevealed, highlightUnrevealed, ownedItemsCache)) { }
/// <summary>Parse gift tastes.</summary> /// <param name="objects">The game's object data.</param> /// <remarks> /// Reverse engineered from <c>Data\NPCGiftTastes</c> and <see cref="StardewValley.NPC.getGiftTasteForThisItem"/>. /// The game decides a villager's gift taste using a complicated algorithm which boils down to the first match out of: /// 1. A villager's personal taste by item ID. /// 2. A universal taste by item ID. /// 3. A villager's personal taste by category. /// 4. A universal taste by category (if not neutral). /// 5. If the item's edibility is less than 0 (but not -300), hate. /// 6. If the item's price is less than 20, dislike. /// 7. If the item is an artifact... /// 7a. and the NPC is Penny, like. /// 7b. else neutral. /// /// For each rule, their tastes are checked in this order: love, hate, like, dislike, or /// neutral. (That is, if an NPC both loves and hates an item, love wins.) /// </remarks> public static IEnumerable <GiftTasteModel> GetGiftTastes(ObjectModel[] objects) { // extract raw values string[] giftableVillagers; var tastes = new List <RawGiftTasteModel>(); { // define data schema var universal = new Dictionary <string, GiftTaste> { ["Universal_Love"] = GiftTaste.Love, ["Universal_Like"] = GiftTaste.Like, ["Universal_Neutral"] = GiftTaste.Neutral, ["Universal_Dislike"] = GiftTaste.Dislike, ["Universal_Hate"] = GiftTaste.Hate }; var personalMetadataKeys = new Dictionary <int, GiftTaste> { // metadata is paired: odd values contain a list of item references, even values contain the reaction dialogue [1] = GiftTaste.Love, [3] = GiftTaste.Like, [5] = GiftTaste.Dislike, [7] = GiftTaste.Hate, [9] = GiftTaste.Neutral }; // get data IDictionary <string, string> data = Game1.NPCGiftTastes; giftableVillagers = data.Keys.Except(universal.Keys).ToArray(); // extract raw tastes foreach (string villager in data.Keys) { string tasteStr = data[villager]; if (universal.ContainsKey(villager)) { GiftTaste taste = universal[villager]; tastes.AddRange( from refID in tasteStr.Split(' ') select new RawGiftTasteModel(taste, "*", int.Parse(refID), isUniversal: true) ); } else { string[] personalData = tasteStr.Split('/'); foreach (KeyValuePair <int, GiftTaste> taste in personalMetadataKeys) { tastes.AddRange( from refID in personalData[taste.Key].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) select new RawGiftTasteModel(taste.Value, villager, int.Parse(refID)) ); } } } } // order by precedence (lower is better) tastes = tastes .OrderBy(entry => { bool isPersonal = !entry.IsUniversal; bool isSpecific = !entry.IsCategory; // precedence between preferences int precedence; switch (entry.Taste) { case GiftTaste.Love: precedence = 1; break; case GiftTaste.Hate: precedence = 2; break; case GiftTaste.Like: precedence = 3; break; case GiftTaste.Dislike: precedence = 4; break; default: precedence = 5; break; } // personal taste by item ID if (isPersonal && isSpecific) { return(10 + precedence); } // else universal taste by item ID if (entry.IsUniversal && isSpecific) { return(20 + precedence); } // else personal taste by category if (isPersonal) { return(30 + precedence); } // else universal taste by category (if not neutral) if (entry.IsUniversal && entry.Taste != GiftTaste.Neutral) { return(40 + precedence); } // else return(50 + precedence); }) .ToList(); // get effective tastes { // get item lookups IDictionary <int, ObjectModel> objectsByID = objects.ToDictionary(p => p.ParentSpriteIndex); IDictionary <int, int[]> objectsByCategory = ( from entry in objects where entry.Category < 0 group entry by entry.Category into items select new { Category = items.Key, Items = items.Select(item => item.ParentSpriteIndex).ToArray() } ) .ToDictionary(p => p.Category, p => p.Items); // get tastes by precedence IDictionary <string, HashSet <int> > seenItemIDs = giftableVillagers.ToDictionary(name => name, name => new HashSet <int>()); foreach (RawGiftTasteModel entry in tastes) { // ignore nonexistent items if (entry.IsCategory && !objectsByCategory.ContainsKey(entry.RefID)) { continue; } if (!entry.IsCategory && !objectsByID.ContainsKey(entry.RefID)) { continue; } // get item IDs int[] itemIDs = entry.IsCategory ? objectsByCategory[entry.RefID] : new[] { entry.RefID }; // get affected villagers string[] villagers = entry.IsUniversal ? giftableVillagers : new[] { entry.Villager }; // yield if no conflict foreach (string villager in villagers) { foreach (int itemID in itemIDs) { // ignore if conflicts with a preceding taste if (seenItemIDs[villager].Contains(itemID)) { continue; } seenItemIDs[villager].Add(itemID); // yield taste yield return(new GiftTasteModel(entry.Taste, villager, itemID)); } } } } }
/********* ** Private methods *********/ /// <summary>Get the text to display.</summary> /// <param name="gameHelper">Provides utility methods for interacting with the game code.</param> /// <param name="giftTastes">The items by how much this NPC likes receiving them.</param> /// <param name="showTaste">The gift taste to show.</param> /// <param name="onlyRevealed">Only show gift tastes the player has discovered for themselves.</param> /// <param name="highlightUnrevealed">Whether to highlight items which haven't been revealed in the NPC profile yet.</param> private static IEnumerable <IFormattedText> GetText(GameHelper gameHelper, IDictionary <GiftTaste, GiftTasteModel[]> giftTastes, GiftTaste showTaste, bool onlyRevealed, bool highlightUnrevealed) { if (!giftTastes.ContainsKey(showTaste)) { yield break; } // get data FoundItem[] ownedItems = gameHelper.GetAllOwnedItems().ToArray(); Item[] inventory = ownedItems.Where(p => p.IsInInventory).Select(p => p.Item).ToArray(); var items = ( from entry in giftTastes[showTaste] let item = entry.Item let isInventory = inventory.Any(p => p.ParentSheetIndex == item.ParentSheetIndex && p.Category == item.Category) let isOwned = ownedItems.Any(p => p.Item.ParentSheetIndex == item.ParentSheetIndex && p.Item.Category == item.Category) where !onlyRevealed || entry.IsRevealed orderby isInventory descending, isOwned descending, item.DisplayName select new { Item = item, IsInventory = isInventory, IsOwned = isOwned, isRevealed = entry.IsRevealed } ) .ToArray(); int unrevealed = onlyRevealed ? giftTastes[showTaste].Count(p => !p.IsRevealed) : 0; // generate text if (items.Any()) { for (int i = 0, last = items.Length - 1; i <= last; i++) { var entry = items[i]; string text = i != last ? entry.Item.DisplayName + ", " : entry.Item.DisplayName; bool bold = highlightUnrevealed && !entry.isRevealed; if (entry.IsInventory) { yield return(new FormattedText(text, Color.Green, bold)); } else if (entry.IsOwned) { yield return(new FormattedText(text, Color.Black, bold)); } else { yield return(new FormattedText(text, Color.Gray, bold)); } } if (unrevealed > 0) { yield return(new FormattedText(I18n.Npc_UndiscoveredGiftTasteAppended(count: unrevealed), Color.Gray)); } } else { yield return(new FormattedText(I18n.Npc_UndiscoveredGiftTaste(count: unrevealed), Color.Gray)); } }