/*********
 ** 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;
 }
示例#2
0
        /// <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
        }
示例#3
0
 /*********
 ** 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);
        }
示例#5
0
        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);
 }
示例#10
0
        /*********
        ** 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));
        }
示例#11
0
        public int GiftTasteRating(NpcHandler npcHandler)
        {
            if (Item == null)
            {
                return(0);
            }

            GiftTaste giftTaste = new GiftTaste(Item, npcHandler);

            return(giftTaste.Rating);
        }
示例#12
0
        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[] { });
 }
示例#14
0
        /// <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));
            }
        }
示例#15
0
        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);
            }
        }
示例#19
0
 public virtual IEnumerable <int> GetUniversalGifts(string npcName, GiftTaste taste)
 {
     return(Database.GetGiftsForTaste(Utils.UniversalTasteNames[taste], taste)
            .Where(itemId => Utils.GetTasteForGift(npcName, itemId) == taste));
 }
示例#20
0
        // 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))
 {
 }
示例#22
0
 /*********
 ** 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))
 {
 }
示例#23
0
        /// <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));
            }
        }
示例#24
0
 /*********
 ** 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;
 }
示例#25
0
 /// <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));
 }
示例#26
0
        public override IEnumerable <int> GetUniversalGifts(string npcName, GiftTaste taste)
        {
            var universal = Utils.GetItemsForTaste(Utils.UniversalTasteNames[taste], taste);

            return(Database.GetGiftsForTaste(npcName, taste).Intersect(universal));
        }
示例#27
0
        /// <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)));
            }
        }
示例#28
0
 /*********
 ** 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))
 {
 }
示例#29
0
        /// <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));
            }
        }