From fa21dc0e04034bec3b23dc222475ca9171c4d5c4 Mon Sep 17 00:00:00 2001 From: Justinas Launikonis <95593194+JustinasLa@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:51:58 +0200 Subject: [PATCH] feat: thicken sauces with a roux cooked from flour in a buttered frying pan The saucepan's Thick tag counted category "flour", but milled flour is minted as category "grain" and canAdd never admitted it, so sauce_thickness thick was unreachable. - Flour in a frying pan turns into a roux (types.yml cooking-option output); without butter it burns into a burnt roux. - A roux (category thickener, naming.yml categories) takes one of the saucepan's two ingredient places; a second or burnt roux is refused with a reason, raw flour with a hint. - Scooping a sauce with a roux makes it Thick. sauce_thickness is a prefix track, so the sauce is named "Thick Garlic Sauce" and dishes it is ladled onto list it that way. Sauces without a roux get no thickness tag (no "Thin"). - A thickener doesn't name the dish; it is an extra in the sauce's stars. - roux, roux_burnt, roux_rotten and flour_heap(_rotten) IA items; flour shows as a flat heap in the pan. Co-Authored-By: Claude Opus 5.5 --- .../tfmc_cooking/contents/categories.yml | 5 + .../tfmc_cooking/contents/intermediates.yml | 36 ++++ .../models/item/intermediate/flour_heap.json | 6 + .../item/intermediate/flour_heap_rotten.json | 6 + .../models/item/intermediate/roux.json | 6 + .../models/item/intermediate/roux_burnt.json | 6 + .../models/item/intermediate/roux_rotten.json | 6 + .../textures/item/intermediate/flour_heap.png | Bin 0 -> 172 bytes .../item/intermediate/flour_heap_rotten.png | Bin 0 -> 172 bytes .../textures/item/intermediate/roux.png | Bin 0 -> 185 bytes .../textures/item/intermediate/roux_burnt.png | Bin 0 -> 196 bytes .../item/intermediate/roux_rotten.png | Bin 0 -> 185 bytes README.md | 1 + .../cooking/cache/NamingConfig.java | 1 + .../cooking/cooking/CookingReference.java | 3 +- .../cooking/cooking/SauceReference.java | 49 ++--- .../cooking/utils/StationAddonRules.java | 49 +++++ src/main/resources/composition.yml | 1 + src/main/resources/config.yml | 2 + src/main/resources/models.yml | 53 +++++ src/main/resources/naming.yml | 5 + src/main/resources/tags.yml | 1 + src/main/resources/types.yml | 27 +++ .../cooking/CookingReferencesTest.java | 6 +- .../cooking/cooking/SauceThickeningTest.java | 181 ++++++++++++++++++ 25 files changed, 414 insertions(+), 36 deletions(-) create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/flour_heap.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/flour_heap_rotten.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux_burnt.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux_rotten.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/flour_heap.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/flour_heap_rotten.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/roux.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/roux_burnt.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/roux_rotten.png create mode 100644 src/test/java/net/tfminecraft/cooking/cooking/SauceThickeningTest.java diff --git a/ItemsAdder/tfmc_cooking/contents/categories.yml b/ItemsAdder/tfmc_cooking/contents/categories.yml index 75c5af0..f363bde 100644 --- a/ItemsAdder/tfmc_cooking/contents/categories.yml +++ b/ItemsAdder/tfmc_cooking/contents/categories.yml @@ -280,6 +280,11 @@ categories: - tfmc_cooking:cutting_knife - tfmc_cooking:flour - tfmc_cooking:flour_rotten + - tfmc_cooking:flour_heap + - tfmc_cooking:flour_heap_rotten + - tfmc_cooking:roux + - tfmc_cooking:roux_burnt + - tfmc_cooking:roux_rotten - tfmc_cooking:flour_model - tfmc_cooking:flour_rotten_model - tfmc_cooking:yeast diff --git a/ItemsAdder/tfmc_cooking/contents/intermediates.yml b/ItemsAdder/tfmc_cooking/contents/intermediates.yml index 7a808d1..47166d2 100644 --- a/ItemsAdder/tfmc_cooking/contents/intermediates.yml +++ b/ItemsAdder/tfmc_cooking/contents/intermediates.yml @@ -43,6 +43,42 @@ items: generate: false model_path: item/intermediate/flour_rotten + # Thick sauce: the heap flour shows as in the frying pan, and the roux it cooks into there. + flour_heap: + display_name: "Flour Heap" + resource: + material: PAPER + generate: false + model_path: item/intermediate/flour_heap + + flour_heap_rotten: + display_name: "Flour Heap Musty" + resource: + material: PAPER + generate: false + model_path: item/intermediate/flour_heap_rotten + + roux: + display_name: "Roux" + resource: + material: PAPER + generate: false + model_path: item/intermediate/roux + + roux_burnt: + display_name: "Roux Burnt" + resource: + material: PAPER + generate: false + model_path: item/intermediate/roux_burnt + + roux_rotten: + display_name: "Roux Rotten" + resource: + material: PAPER + generate: false + model_path: item/intermediate/roux_rotten + yeast: display_name: "Yeast" resource: diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/flour_heap.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/flour_heap.json new file mode 100644 index 0000000..b45a81c --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/flour_heap.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/flour_heap" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/flour_heap_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/flour_heap_rotten.json new file mode 100644 index 0000000..a206bbd --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/flour_heap_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/flour_heap_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux.json new file mode 100644 index 0000000..9d05bba --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/roux" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux_burnt.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux_burnt.json new file mode 100644 index 0000000..a7c555c --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux_burnt.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/roux_burnt" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux_rotten.json new file mode 100644 index 0000000..b3ba8ff --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/roux_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/roux_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/flour_heap.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/flour_heap.png new file mode 100644 index 0000000000000000000000000000000000000000..883414cad7e5f8b891c716a10aaf93a53446eee0 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`#hxyXAr-fh6C|c29Ng0O|AxUu zDf>OUSdabxU&op(A>UFc)wV`3(s&o6Le2A2j3Le!OqHdZeCK2^7bH$#EO7R?>a&s2 z;H-O@uA3=?ipQEX5$1$MhRF&b9W4eP#%+uzIysgxh^x)eDpXBgG}-trBSX(auIWu> Su3bRu89ZJ6T-G@yGywqTayRb) literal 0 HcmV?d00001 diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/flour_heap_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/flour_heap_rotten.png new file mode 100644 index 0000000000000000000000000000000000000000..bb4e5b72b72f2459bb2f335340b7b035e8f7ed07 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`#hxyXAr-fh6C|cAs4jX|pEDz) zH+=p&rpM~{%bN{*_zzj|I$!HpBYBrWz^!2Af3ZUZZ+eJwg-n4dLv91tEbLz jD)A|rr(kJ3k(uGamV7gaPB!vU jv{JM5pD<_U2}XvCMr)_leysHXx`n~h)z4*}Q$iB}O;SR% literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 43e1e69..9caad68 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Cooking makes food an interactive part of everyday life. Players grow and gather - **Pizzas** — place a pizza peel, put dough on it, spread a paste and add cheese, vegetables, fruit, sausage and spices, then carry the peel into a lit oven to bake. A baked pizza is taken off a slice at a time. Its toppings name it, and its food, quality and freshness come from its layers (`pizzas.yml`). Its toppings also earn it bonuses such as Saucy, Extra Cheesy or Well Seasoned (`bonuses` in `pizzas.yml`). - **Salads** — right-click a placed bowl with chopped or cooked veg, fruit, cheese or meat, add a dressing or seasoning, and toss it with an empty hand. Each ingredient lies in the bowl as a small heap of its pieces, in its own look for every origin and state (`models` and `spread-radius`); the tossed salad stays in the bowl with them and is eaten from it. Its ingredients name it, its kind (fruit, greek, carrot, mixed) gives it its look, and it earns bonuses such as Dressed or Crisp (`salads.yml`). - **Stock and mashed potatoes** — boil two bones in a cup of water in the saucepan and bottle the stock (`condiments.yml`); a soup started with stock instead of water is Rich. Put four boiled cut potatoes and a butter (with milk or salt if you like) in an empty pot, mash them without water or heat, and ladle the mash onto a plate beside the other food (`pot.yml`). +- **Thick sauce** — cook flour in a buttered frying pan until it turns into a roux (without butter it burns, and a burnt roux spoils a sauce). A roux put into a saucepan of water, milk or a brew takes one of its two ingredient places and makes the sauce Thick: the tag names it (Thick Garlic Sauce), so a dish it is ladled onto lists a Thick sauce too. Raw flour is refused with a hint (`types.yml` flour `output`, `naming.yml` `thickener`, `tags.yml` `sauce_thickness`). - **Pies** — line a pie tin with dough, fill it with fruit, meat, mashed potato, vegetables or cheese, and bake it in the oven like the bread tray. The whole pie comes out of the tin with an empty hand and is cut into four slices on the cutting board. Its fillings make its kind (Apple, Key Lime, Mixed Berry, Meat, Shepherd's) and name, and its food, quality and freshness come from its dough and fillings (`pies.yml`). - **Mushrooms and hidden poison** — grilled mushrooms go into sandwiches and salads, raw or grilled ones onto pizzas and into pies, and raw ones boil into soup; in a dish both kinds are just "Mushroom". Each red mushroom poisons the dish a step further (Tainted, Poisoned, Deadly: less and less nutrition, the same food, and nausea, then poison, then weakness for whoever eats it). In a dish of 3 stars or more with its red mushrooms grilled (or its own bake cooked, its soup boiled) the poison is hidden: the lore shows no tag and the Food and Nutrition it would have without it, but eating applies it. Any dish bonus can count an origin and hide its tag this way (`count: origin`, `hide-at-stars`, `hide-tags`, `hide-dish-tags` in the dishes' `bonuses`; `tags.yml` poison). Any tag step can make the eater ill with `effects` (`"poison 10 1"`: effect, seconds, level), hidden or not. - **Serving and sharing** — plate dishes, ladle soup into bowls, and carve foods into portions for the table. diff --git a/src/main/java/net/tfminecraft/cooking/cache/NamingConfig.java b/src/main/java/net/tfminecraft/cooking/cache/NamingConfig.java index cead0c9..7610a35 100644 --- a/src/main/java/net/tfminecraft/cooking/cache/NamingConfig.java +++ b/src/main/java/net/tfminecraft/cooking/cache/NamingConfig.java @@ -25,6 +25,7 @@ private static Map> defaultCategories() { map.put("sweetener", Set.of("sweetener")); map.put("addon", Set.of("garnish", "spice")); map.put("dough-filler", Set.of("fruit")); + map.put("thickener", Set.of("thickener")); return map; } diff --git a/src/main/java/net/tfminecraft/cooking/cooking/CookingReference.java b/src/main/java/net/tfminecraft/cooking/cooking/CookingReference.java index 3251364..6c11498 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/CookingReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/CookingReference.java @@ -306,7 +306,8 @@ public List getIngredients() { String category = fi.getCategory().toLowerCase(); - if (NamingConfig.isCategory("seasoning", category)) { + // A seasoning or a thickener (a roux) doesn't name the dish; Seasoned and Thick tags say it's there. + if (NamingConfig.isCategory("seasoning", category) || NamingConfig.isCategory("thickener", category)) { continue; } diff --git a/src/main/java/net/tfminecraft/cooking/cooking/SauceReference.java b/src/main/java/net/tfminecraft/cooking/cooking/SauceReference.java index 02b9ede..2512582 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/SauceReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/SauceReference.java @@ -216,12 +216,26 @@ public boolean canAdd(Player player, ItemStack i) { + if (StationAddonRules.isThickenerCategory(category)) { + + return StationAddonRules.canAcceptThickener(slots, fi, player); + + } + + + if (category.equals("alcohol")) return true; // What a bottling recipe for this liquid still needs (tomato paste in vinegar for catsup). - return bottlingTakes(fi); + if (bottlingTakes(fi)) return true; + + // Raw flour doesn't thicken a sauce; a roux does. + if (ItemCache.isFlour(i) && player != null) { + player.sendMessage("§7Cook the flour in a buttered frying pan first: a roux thickens the sauce."); + } + return false; } @@ -241,26 +255,10 @@ public void scoop(Player p, ItemStack ladle) { - int flourCount = 0; - boolean hasMilk = false; - for (FoodItem fi : slots.values()) { - - String category = fi.getCategory().toLowerCase(); - - if (category.equals("flour")) { - - flourCount++; - - } - - } - - - for (String c : colours) { // A white brew (vodka) is not milk. @@ -284,21 +282,8 @@ public void scoop(Player p, ItemStack ladle) { StationAddonRules.applySweetTag(sauce, slots); - - - TagTrack thickTrack = new TagTrack(TrackLoader.getByString("sauce_thickness")); - - if (flourCount >= 1) { - - thickTrack.setValue(1); - - } else { - - thickTrack.setValue(0); - - } - - sauce.addOrModifyTrack(thickTrack); + // A roux makes it Thick, which also names it (naming.yml prefix-tracks), so dishes it's ladled onto say so. + StationAddonRules.applyThickTag(sauce, slots); diff --git a/src/main/java/net/tfminecraft/cooking/utils/StationAddonRules.java b/src/main/java/net/tfminecraft/cooking/utils/StationAddonRules.java index 3094ab6..46e5ace 100644 --- a/src/main/java/net/tfminecraft/cooking/utils/StationAddonRules.java +++ b/src/main/java/net/tfminecraft/cooking/utils/StationAddonRules.java @@ -8,6 +8,7 @@ import org.bukkit.entity.Player; import net.tfminecraft.cooking.cache.NamingConfig; +import net.tfminecraft.cooking.enums.Tag; import net.tfminecraft.cooking.item.FoodItem; import net.tfminecraft.cooking.item.tag.TagTrack; import net.tfminecraft.cooking.loader.TrackLoader; @@ -34,6 +35,54 @@ public static boolean isSweetenerCategory(String category) { return NamingConfig.isCategory("sweetener", category); } + /** A thickener (naming.yml categories: thickener), a roux cooked from flour in a buttered frying pan. */ + public static boolean isThickenerCategory(String category) { + return NamingConfig.isCategory("thickener", category); + } + + public static boolean hasThickener(Map slots) { + for (FoodItem item : slots.values()) { + if (item != null && isThickenerCategory(item.getCategory())) { + return true; + } + } + return false; + } + + /** One thickener per batch, and never a burnt one. */ + public static boolean canAcceptThickener(Map slots, FoodItem incoming, Player player) { + if (incoming == null || !isThickenerCategory(incoming.getCategory())) { + return false; + } + String refusal = null; + if (incoming.hasTag(Tag.BURNT)) { + refusal = "§cThat roux is burnt. It would spoil the sauce."; + } else if (hasThickener(slots)) { + refusal = "§cThere's already a roux in there."; + } + if (refusal == null) { + return true; + } + if (player != null) { + player.sendMessage(refusal); + } + return false; + } + + /** A thickener makes the sauce Thick (tags.yml sauce_thickness); without one the sauce gets no thickness tag. */ + public static void applyThickTag(FoodItem product, Map slots) { + if (!hasThickener(slots)) { + return; + } + TagTrack track = TrackLoader.getByString("sauce_thickness"); + if (track == null) { + return; + } + track = new TagTrack(track); + track.setValue(1); + product.addOrModifyTrack(track); + } + public static boolean hasSweetener(Map slots) { for (FoodItem item : slots.values()) { if (item != null && isSweetenerCategory(item.getCategory())) { diff --git a/src/main/resources/composition.yml b/src/main/resources/composition.yml index b08153e..33b2e6f 100644 --- a/src/main/resources/composition.yml +++ b/src/main/resources/composition.yml @@ -24,6 +24,7 @@ context-overrides: - pepper - sweetener - flour + - thickener SOUP_SCOOP: extra: - salt diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 4050983..a4a9431 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -164,6 +164,7 @@ colours: - origin.pistachio 93c47d - origin.rhubarb c45c6a - origin.garlic f5f0e6 + - origin.roux d9a95b dictionary: @@ -180,6 +181,7 @@ dictionary: sauce: "#997950Sauce" garnish: "#0b5394Garnish" liquid: "#3d85c6Liquid" + thickener: "#d9a95bThickener" snack: "#e8c56bSnack" sauce-dict: diff --git a/src/main/resources/models.yml b/src/main/resources/models.yml index c18f3f1..748bede 100644 --- a/src/main/resources/models.yml +++ b/src/main/resources/models.yml @@ -3069,6 +3069,17 @@ melon_slice: y: -0.25 flour: + # In the frying pan (types.yml flour's roux output) flour shows as a flat heap, placed like the roux it becomes. + display-data-furniture: + frying_pan: + rotation: + x: 90 + scale: + x: 0.45 + y: 0.45 + z: 0.45 + position: + y: -0.26 fresh: weight: 1 tags: @@ -3077,6 +3088,8 @@ flour: item: ia.tfmc_cooking:flour display: item: ia.tfmc_cooking:flour_model + furniture: + - frying_pan ia.tfmc_cooking:flour_heap stale: weight: 1 tags: @@ -3085,6 +3098,8 @@ flour: item: ia.tfmc_cooking:flour display: item: ia.tfmc_cooking:flour_model + furniture: + - frying_pan ia.tfmc_cooking:flour_heap rotten: weight: 1 tags: @@ -3093,6 +3108,44 @@ flour: item: ia.tfmc_cooking:flour_rotten display: item: ia.tfmc_cooking:flour_rotten_model + furniture: + - frying_pan ia.tfmc_cooking:flour_heap_rotten + +# Roux (types.yml roux), flour cooked in a buttered frying pan. In the pan it lies flat like popcorn, lowered to the pan +# floor, at 45% so two round puddles fit side by side. +roux: + display-data-furniture: + frying_pan: + rotation: + x: 90 + scale: + x: 0.45 + y: 0.45 + z: 0.45 + position: + y: -0.26 + cooked: + weight: 1 + gui: + item: ia.tfmc_cooking:roux + display: + item: ia.tfmc_cooking:roux + burnt: + weight: 2 + tags: + - burnt + gui: + item: ia.tfmc_cooking:roux_burnt + display: + item: ia.tfmc_cooking:roux_burnt + rotten: + weight: 3 + tags: + - rotten + gui: + item: ia.tfmc_cooking:roux_rotten + display: + item: ia.tfmc_cooking:roux_rotten yeast: fresh: diff --git a/src/main/resources/naming.yml b/src/main/resources/naming.yml index c79a437..d5fe777 100644 --- a/src/main/resources/naming.yml +++ b/src/main/resources/naming.yml @@ -8,6 +8,8 @@ prefix-tracks: - topping_sugar - topping_salt - topping_caramel + # A sauce with a roux: Thick Garlic Sauce, which is also how a dish it's ladled onto lists it. + - sauce_thickness # Max filler origins shown in a composed name fillers: @@ -37,6 +39,9 @@ categories: - spice dough-filler: - fruit + # What thickens a sauce in the saucepan (one per batch): a roux. + thickener: + - thickener # Per-category tag step display overrides (fallback after per-type tag-labels) tag-label-defaults: diff --git a/src/main/resources/tags.yml b/src/main/resources/tags.yml index eea880f..7de8b55 100644 --- a/src/main/resources/tags.yml +++ b/src/main/resources/tags.yml @@ -124,6 +124,7 @@ sauce_creamy: value: 0 nutrition-mult: 1.1 food-mult: 1.05 +# A sauce with a roux in it (SauceReference) is Thick; without one it gets no thickness tag. Thin is kept for food strings. sauce_thickness: thin: name: "#bfb999Thin" diff --git a/src/main/resources/types.yml b/src/main/resources/types.yml index 803ad37..5a2fada 100644 --- a/src/main/resources/types.yml +++ b/src/main/resources/types.yml @@ -492,6 +492,16 @@ flour: model: flour food: 4 nutrition: 4 + # Thick sauce: flour cooked in a buttered frying pan becomes a roux (the butter goes into it, as in any frying). + # Without butter it burns, and a burnt roux won't go in a sauce. Flour elsewhere keeps its values: a type whose only + # options have an output isn't raw until it's in the pan. The pan's slot whitelist (InteractibleFurniture) must let + # flour in: c.grain(type=flour). + cooking-options: + frying_pan: + tag: 1 + time: 12 + burn: 30 + output: thickener(type=roux;origin=Roux;tags=processed.0) age: warmth: 450 freshness: 20 @@ -533,6 +543,23 @@ sauce: food: 6 nutrition: 5 +###Roux: flour cooked in a buttered frying pan (flour's frying_pan output). Added to a saucepan of water, milk or a brew +# it takes one of the two ingredient places and makes the sauce Thick (naming.yml categories: thickener). It doesn't +# name the sauce; the Thick tag does: Thick Garlic Sauce. +roux: + name: "#d9a95bRoux" + model: roux + category: thickener + food: 6 + nutrition: 4 + age: + warmth: 450 + freshness: 14 + tag-labels: + freshness: + stale: "#c3db81Rancid" + rotten: "#8B7355Spoiled" + ##SOUP soup: age: diff --git a/src/test/java/net/tfminecraft/cooking/cooking/CookingReferencesTest.java b/src/test/java/net/tfminecraft/cooking/cooking/CookingReferencesTest.java index 89b54ce..a9fe966 100644 --- a/src/test/java/net/tfminecraft/cooking/cooking/CookingReferencesTest.java +++ b/src/test/java/net/tfminecraft/cooking/cooking/CookingReferencesTest.java @@ -403,12 +403,12 @@ void stackedSauceContainersReturnExtrasOrDropThemWhenTheInventoryIsFull(String m } @Test - void sauceScoopCombinesQualityAndFlourMilkTagsThenClearsTheStation() { + void sauceScoopCombinesQualityAndRouxMilkTagsThenClearsTheStation() { SauceReference ref = new SauceReference(env.furniture, Method.SAUCEPAN); ref.scoop(env.player, new ItemStack(Material.PAPER)); env.player.getInventory().setItemInMainHand(env.stack(env.food("milk", "milk", "Milk"), Material.WHITE_DYE, 1)); ref.interact(env.interaction()); - ref.slots.put("input_1", env.food("flour", "flour", "Wheat")); + ref.slots.put("input_1", env.food("roux", "thickener", "Roux")); ref.slots.put("input_2", env.food("sugar", "sweetener", "Sugar")); ref.colours.add("ffffff"); ref.scoop(env.player, new ItemStack(Material.PAPER)); @@ -424,7 +424,7 @@ void sauceScoopCombinesQualityAndFlourMilkTagsThenClearsTheStation() { ref.secondaries.put("liquid", -1); ref.colours.add("aa5500"); ref.scoop(env.player, new ItemStack(Material.PAPER)); - assertEquals(0, env.lastBuilt.getTagTrack("sauce_thickness").getValue()); + assertFalse(env.lastBuilt.hasTagTrack("sauce_thickness")); assertFalse(env.lastBuilt.hasTagTrack("sauce_creamy")); } diff --git a/src/test/java/net/tfminecraft/cooking/cooking/SauceThickeningTest.java b/src/test/java/net/tfminecraft/cooking/cooking/SauceThickeningTest.java new file mode 100644 index 0000000..75d1dcb --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/cooking/SauceThickeningTest.java @@ -0,0 +1,181 @@ +package net.tfminecraft.cooking.cooking; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.cooking.cache.ItemCache; +import net.tfminecraft.cooking.cache.NamingConfig; +import net.tfminecraft.cooking.cooking.CookingReferencesTest.Environment; +import net.tfminecraft.cooking.enums.Method; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.loader.FoodLoader; +import net.tfminecraft.cooking.loader.NamingLoader; +import net.tfminecraft.cooking.loader.TrackLoader; +import net.tfminecraft.cooking.utils.StationAddonRules; + +/** Thick sauce: a roux (flour cooked in a buttered frying pan) in the saucepan makes the sauce Thick and names it so. */ +class SauceThickeningTest { + static final String THICK_MESSAGE_BURNT = "§cThat roux is burnt. It would spoil the sauce."; + static final String THICK_MESSAGE_TWICE = "§cThere's already a roux in there."; + + Environment env; + SauceReference pan; + + @BeforeEach void open() { + env = new Environment(); + pan = new SauceReference(env.furniture, Method.SAUCEPAN); + // As naming.yml ships it: Sweet ... Thick prefixes, and the default categories (which include thickener). + NamingConfig.apply(List.of("sweet", "sauce_thickness"), 3, 2, 2, null, null, null); + } + + @AfterEach void close() { + NamingConfig.apply(List.of("sweet"), 3, 2, 2, null, null, null); + env.close(); + } + + FoodItem roux() { + return env.food("roux", "thickener", "Roux"); + } + + void hold(FoodItem food, Material material, int amount) { + env.player.getInventory().setItemInMainHand(env.stack(food, material, amount)); + } + + void pourMilk() { + hold(env.food("milk", "dairy", "Milk"), Material.WHITE_DYE, 1); + pan.interact(env.interaction()); + assertEquals(-1, pan.getSecondary("liquid")); + } + + @SuppressWarnings("deprecation") + String heldName() { + return env.player.getInventory().getItemInMainHand().getItemMeta().getDisplayName(); + } + + @Test void aRouxTakesOneOfTheTwoPlacesAndMakesAThickSauceThatItDoesNotName() { + pourMilk(); + hold(roux(), Material.BROWN_DYE, 2); + pan.interact(env.interaction()); + assertEquals(1, pan.getSlots().size(), "The roux takes one place"); + assertEquals(1, env.player.getInventory().getItemInMainHand().getAmount()); + hold(env.food("garlic", "garnish", "Garlic"), Material.GREEN_DYE, 1); + pan.interact(env.interaction()); + assertEquals(2, pan.getSlots().size(), "The other place is still free for an addition"); + + env.player.getInventory().setItemInMainHand(new ItemStack(Material.PAPER)); + pan.interact(env.interaction()); + FoodItem sauce = env.lastBuilt; + assertEquals(1, sauce.getTagTrack("sauce_thickness").getValue()); + assertTrue(sauce.getTagTrack("sauce_thickness").getCurrentStep().getName().endsWith("Thick")); + String name = heldName(); + assertTrue(name.contains("Thick Garlic"), name); + assertFalse(name.contains("Roux"), "A thickener doesn't name the sauce: " + name); + assertTrue(pan.isEmpty()); + } + + @Test void aSauceWithoutARouxHasNoThicknessAndNoThinInItsName() { + pourMilk(); + env.player.getInventory().setItemInMainHand(new ItemStack(Material.PAPER)); + pan.interact(env.interaction()); + assertFalse(env.lastBuilt.hasTagTrack("sauce_thickness")); + assertFalse(heldName().contains("Thin"), heldName()); + } + + @Test void aSecondRouxAndABurntRouxAreRefusedWithAReason() { + pourMilk(); + FoodItem burnt = env.food("roux", "thickener", "Roux", Method.FRYING_PAN); + burnt.getTagTrack("cooked").setValue(2); + hold(burnt, Material.BROWN_DYE, 1); + pan.interact(env.interaction()); + assertTrue(pan.getSlots().isEmpty()); + verify(env.player).sendMessage(THICK_MESSAGE_BURNT); + + hold(roux(), Material.BROWN_DYE, 2); + pan.interact(env.interaction()); + pan.interact(env.interaction()); + assertEquals(1, pan.getSlots().size()); + assertEquals(1, env.player.getInventory().getItemInMainHand().getAmount()); + verify(env.player).sendMessage(THICK_MESSAGE_TWICE); + } + + @Test void rawFlourIsRefusedWithAHintAndOtherFoodSilently() { + pourMilk(); + env.cache.when(() -> ItemCache.isFlour(any())).thenAnswer(inv -> Environment.material(inv.getArgument(0), Material.SUGAR)); + hold(env.food("flour", "grain", "Wheat"), Material.SUGAR, 1); + pan.interact(env.interaction()); + assertTrue(pan.getSlots().isEmpty()); + verify(env.player).sendMessage("§7Cook the flour in a buttered frying pan first: a roux thickens the sauce."); + assertFalse(pan.canAdd(null, env.stack(env.food("flour", "grain", "Wheat"), Material.SUGAR, 1))); + + hold(env.food("bread", "bread", "Wheat"), Material.BREAD, 1); + pan.interact(env.interaction()); + assertTrue(pan.getSlots().isEmpty()); + verify(env.player, times(1)).sendMessage(anyString()); + } + + @Test void thickenerRulesStandAloneAndWithoutTheirTrack() { + Map slots = new HashMap<>(); + slots.put("empty", null); + assertFalse(StationAddonRules.hasThickener(slots)); + assertFalse(StationAddonRules.canAcceptThickener(slots, null, null)); + assertFalse(StationAddonRules.canAcceptThickener(slots, env.food("salt", "salt", "Salt"), null)); + FoodItem burnt = env.food("roux", "thickener", "Roux", Method.FRYING_PAN); + burnt.getTagTrack("cooked").setValue(2); + assertFalse(StationAddonRules.canAcceptThickener(slots, burnt, null)); + assertTrue(StationAddonRules.canAcceptThickener(slots, roux(), null)); + + FoodItem sauce = env.food("sauce", "sauce", "Mixed"); + StationAddonRules.applyThickTag(sauce, slots); + assertFalse(sauce.hasTagTrack("sauce_thickness")); + slots.put("input_1", roux()); + assertTrue(StationAddonRules.hasThickener(slots)); + TrackLoader.oList.removeIf(track -> track.getId().equals("sauce_thickness")); + StationAddonRules.applyThickTag(sauce, slots); + assertFalse(sauce.hasTagTrack("sauce_thickness"), "No sauce_thickness track in tags.yml leaves the sauce as it is"); + } + + @Test void theShippedConfigCooksFlourIntoARouxThatThickensAndNamesSauces() throws Exception { + new FoodLoader().load(new File("src/main/resources/types.yml")); + FoodItem flour = new FoodItem(FoodLoader.getByString("flour")); + assertEquals("thickener(type=roux;origin=Roux;tags=processed.0)", + flour.getCookData().getParameters().get(Method.FRYING_PAN).getOutput()); + assertFalse(flour.hasTagTrack("cooked"), "Flour keeps its values outside the pan"); + FoodItem roux = FoodLoader.getByString("roux"); + assertTrue(roux.getCookData().getParameters().isEmpty(), "A roux doesn't cook on"); + + new NamingLoader().load(new File("src/main/resources/naming.yml")); + assertTrue(StationAddonRules.isThickenerCategory("thickener")); + assertTrue(NamingConfig.getPrefixTracks().contains("sauce_thickness")); + assertTrue(StationAddonRules.isThickenerCategory("THICKENER")); + + String models = Files.readString(Path.of("src/main/resources/models.yml")); + String composition = Files.readString(Path.of("src/main/resources/composition.yml")); + String items = Files.readString(Path.of("ItemsAdder/tfmc_cooking/contents/intermediates.yml")); + String categories = Files.readString(Path.of("ItemsAdder/tfmc_cooking/contents/categories.yml")); + assertTrue(models.contains("\nroux:\n")); + assertTrue(models.contains(" - frying_pan ia.tfmc_cooking:flour_heap\n")); + assertTrue(composition.contains(" - flour\n - thickener\n"), "A roux is an extra in a sauce's stars"); + for (String id : List.of("roux", "roux_burnt", "roux_rotten", "flour_heap", "flour_heap_rotten")) { + assertTrue(models.contains("ia.tfmc_cooking:" + id + "\n"), id); + assertTrue(items.contains("\n " + id + ":\n"), id); + assertTrue(categories.contains(" - tfmc_cooking:" + id + "\n"), id); + Path pack = Path.of("ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking"); + assertTrue(Files.exists(pack.resolve("models/item/intermediate/" + id + ".json")), id); + assertTrue(Files.exists(pack.resolve("textures/item/intermediate/" + id + ".png")), id); + } + } +}