diff --git a/ItemsAdder/tfmc_cooking/contents/categories.yml b/ItemsAdder/tfmc_cooking/contents/categories.yml index 75c5af0..6238fc4 100644 --- a/ItemsAdder/tfmc_cooking/contents/categories.yml +++ b/ItemsAdder/tfmc_cooking/contents/categories.yml @@ -323,6 +323,23 @@ categories: - tfmc_cooking:popcorn_buttered - tfmc_cooking:popcorn_machine - tfmc_cooking:rice + - tfmc_cooking:rice_rotten + - tfmc_cooking:rice_grains + - tfmc_cooking:boiled_rice + - tfmc_cooking:boiled_rice_burnt + - tfmc_cooking:boiled_rice_rotten + - tfmc_cooking:boiled_rice_heap + - tfmc_cooking:boiled_rice_heap_burnt + - tfmc_cooking:boiled_rice_heap_rotten + - tfmc_cooking:fried_rice + - tfmc_cooking:fried_rice_burnt + - tfmc_cooking:fried_rice_rotten + - tfmc_cooking:fried_rice_heap + - tfmc_cooking:fried_rice_heap_burnt + - tfmc_cooking:rice_pudding + - tfmc_cooking:rice_pudding_rotten + - tfmc_cooking:sushi + - tfmc_cooking:sushi_rotten - tfmc_cooking:stock - tfmc_cooking:sunflower_seeds - tfmc_cooking:strawberry_jam diff --git a/ItemsAdder/tfmc_cooking/contents/intermediates.yml b/ItemsAdder/tfmc_cooking/contents/intermediates.yml index 7a808d1..071b9bd 100644 --- a/ItemsAdder/tfmc_cooking/contents/intermediates.yml +++ b/ItemsAdder/tfmc_cooking/contents/intermediates.yml @@ -176,6 +176,125 @@ items: generate: false model_path: item/intermediate/rice + rice_rotten: + display_name: "Rice Rotten" + resource: + material: PAPER + generate: false + model_path: item/intermediate/rice_rotten + + rice_grains: + display_name: "Rice Grains" + resource: + material: PAPER + generate: false + model_path: item/intermediate/rice_grains + + boiled_rice: + display_name: "Boiled Rice" + resource: + material: APPLE + generate: false + model_path: item/intermediate/boiled_rice + + boiled_rice_burnt: + display_name: "Boiled Rice Burnt" + resource: + material: APPLE + generate: false + model_path: item/intermediate/boiled_rice_burnt + + boiled_rice_rotten: + display_name: "Boiled Rice Rotten" + resource: + material: APPLE + generate: false + model_path: item/intermediate/boiled_rice_rotten + + boiled_rice_heap: + display_name: "Boiled Rice Heap" + resource: + material: PAPER + generate: false + model_path: item/intermediate/boiled_rice_heap + + boiled_rice_heap_burnt: + display_name: "Boiled Rice Heap Burnt" + resource: + material: PAPER + generate: false + model_path: item/intermediate/boiled_rice_heap_burnt + + boiled_rice_heap_rotten: + display_name: "Boiled Rice Heap Rotten" + resource: + material: PAPER + generate: false + model_path: item/intermediate/boiled_rice_heap_rotten + + fried_rice: + display_name: "Fried Rice" + resource: + material: APPLE + generate: false + model_path: item/intermediate/fried_rice + + fried_rice_burnt: + display_name: "Fried Rice Burnt" + resource: + material: APPLE + generate: false + model_path: item/intermediate/fried_rice_burnt + + fried_rice_rotten: + display_name: "Fried Rice Rotten" + resource: + material: APPLE + generate: false + model_path: item/intermediate/fried_rice_rotten + + fried_rice_heap: + display_name: "Fried Rice Heap" + resource: + material: PAPER + generate: false + model_path: item/intermediate/fried_rice_heap + + fried_rice_heap_burnt: + display_name: "Fried Rice Heap Burnt" + resource: + material: PAPER + generate: false + model_path: item/intermediate/fried_rice_heap_burnt + + rice_pudding: + display_name: "Rice Pudding" + resource: + material: PAPER + generate: false + model_path: item/intermediate/rice_pudding + + rice_pudding_rotten: + display_name: "Rice Pudding Rotten" + resource: + material: PAPER + generate: false + model_path: item/intermediate/rice_pudding_rotten + + sushi: + display_name: "Sushi" + resource: + material: APPLE + generate: false + model_path: item/intermediate/sushi + + sushi_rotten: + display_name: "Sushi Rotten" + resource: + material: APPLE + generate: false + model_path: item/intermediate/sushi_rotten + stock: display_name: "Stock" resource: diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice.json new file mode 100644 index 0000000..a7c9f33 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/boiled_rice" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_burnt.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_burnt.json new file mode 100644 index 0000000..bce33ce --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_burnt.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/boiled_rice_burnt" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap.json new file mode 100644 index 0000000..0723d60 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/boiled_rice_heap" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap_burnt.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap_burnt.json new file mode 100644 index 0000000..bfe3c59 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap_burnt.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/boiled_rice_heap_burnt" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap_rotten.json new file mode 100644 index 0000000..cc6f72a --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/boiled_rice_heap_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_rotten.json new file mode 100644 index 0000000..21bdab9 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/boiled_rice_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice.json new file mode 100644 index 0000000..498bcc6 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/fried_rice" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_burnt.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_burnt.json new file mode 100644 index 0000000..3448fea --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_burnt.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/fried_rice_burnt" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_heap.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_heap.json new file mode 100644 index 0000000..a7bc4d9 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_heap.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/fried_rice_heap" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_heap_burnt.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_heap_burnt.json new file mode 100644 index 0000000..fce1b80 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_heap_burnt.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/fried_rice_heap_burnt" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_rotten.json new file mode 100644 index 0000000..8ac3360 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/fried_rice_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_grains.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_grains.json new file mode 100644 index 0000000..5987122 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_grains.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/rice_grains" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_pudding.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_pudding.json new file mode 100644 index 0000000..210e7b6 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_pudding.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/rice_pudding" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_pudding_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_pudding_rotten.json new file mode 100644 index 0000000..0e5fbca --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_pudding_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/rice_pudding_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_rotten.json new file mode 100644 index 0000000..0bc0c97 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/rice_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sushi.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sushi.json new file mode 100644 index 0000000..c92fc31 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sushi.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/sushi" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sushi_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sushi_rotten.json new file mode 100644 index 0000000..9c0367b --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sushi_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/sushi_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice.png new file mode 100644 index 0000000..20b62c3 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_burnt.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_burnt.png new file mode 100644 index 0000000..da080aa Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_burnt.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap.png new file mode 100644 index 0000000..d564c17 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap_burnt.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap_burnt.png new file mode 100644 index 0000000..7df4d09 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap_burnt.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap_rotten.png new file mode 100644 index 0000000..70f5701 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap_rotten.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_rotten.png new file mode 100644 index 0000000..1e8e586 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_rotten.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice.png new file mode 100644 index 0000000..1626c4c Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_burnt.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_burnt.png new file mode 100644 index 0000000..da080aa Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_burnt.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_heap.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_heap.png new file mode 100644 index 0000000..c6c190b Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_heap.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_heap_burnt.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_heap_burnt.png new file mode 100644 index 0000000..83cea2b Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_heap_burnt.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_rotten.png new file mode 100644 index 0000000..43ab9c3 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_rotten.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_grains.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_grains.png new file mode 100644 index 0000000..ed1f955 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_grains.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_pudding.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_pudding.png new file mode 100644 index 0000000..314222b Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_pudding.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_pudding_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_pudding_rotten.png new file mode 100644 index 0000000..8d3c917 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_pudding_rotten.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_rotten.png new file mode 100644 index 0000000..568bc5b Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_rotten.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sushi.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sushi.png new file mode 100644 index 0000000..a736287 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sushi.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sushi_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sushi_rotten.png new file mode 100644 index 0000000..d72719c Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sushi_rotten.png differ diff --git a/README.md b/README.md index 4070f00..6678a06 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Cooking makes food an interactive part of everyday life. Players grow and gather - **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, Pumpkin, Meat, Shepherd's) and name, and its food, quality and freshness come from its dough and fillings (`pies.yml`). - **Sweet berries, sugar cane and beetroot** — sweet berries become Cooking fruit on pickup (`conversions.yml`), so they go into salads, pies (Mixed Berry) and jam sandwiches (Sweet Berry Jam). The milling stone holds several recipes (`milling-recipes.yml`): 8 wheat grind into flour, and 8 sugar cane into 8 sugar, the same sweetener vanilla sugar becomes on pickup; each recipe sets its result's category, origin and amount, what the stone shows, and what a broken stone gives back. A soup that is at least half beetroot is ladled as Borscht: `pot.yml` `kinds` name a soup from its boiled pieces, as `pies.yml` kinds name pies. - **Pumpkin** — a pie of nothing but cut or chopped pumpkin bakes into a Pumpkin Pie (`pies.yml` kinds), and a soup that is at least half pumpkin is ladled as Pumpkin Soup (`pot.yml` kinds). +- **Rice** — harvested rice boils in the soup pot into boiled rice (a pot option can name an `output` too, as the pan's can). Boiled rice fries into fried rice in the frying pan, and taken out it stirs in the other fried pieces beside it: a fried egg, cut vegetables, mushrooms, meat or fish, which name it (Egg Fried Rice) and add their food (`frying-pan.yml` stir-ins). Two boiled rice, a cup of milk and a sweetener make rice pudding in an empty pot (`pot.yml`), and a boiled rice and a raw fish filet roll into two pieces of sushi named for the fish on the cutting board (Salmon Sushi): a `combine` recipe takes one of each input whatever their origins (`crafting-stations.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. - **Quality and freshness** — ingredient quality and composition influence finished dishes, while aging and food tags carry preparation history through the kitchen. diff --git a/src/main/java/net/tfminecraft/cooking/Cooking.java b/src/main/java/net/tfminecraft/cooking/Cooking.java index 5276077..9ec4fd1 100644 --- a/src/main/java/net/tfminecraft/cooking/Cooking.java +++ b/src/main/java/net/tfminecraft/cooking/Cooking.java @@ -72,6 +72,7 @@ import net.tfminecraft.cooking.pie.PieHandler; import net.tfminecraft.cooking.salad.SaladConfig; import net.tfminecraft.cooking.pot.PotConfig; +import net.tfminecraft.cooking.pan.PanConfig; import net.tfminecraft.cooking.salad.SaladHandler; import net.tfminecraft.cooking.sandwich.SandwichConfig; import net.tfminecraft.cooking.sandwich.SandwichHandler; @@ -151,6 +152,7 @@ public class Cooking extends JavaPlugin { private final PizzaHandler pizzaHandler = new PizzaHandler(); private final SaladConfig saladConfig = new SaladConfig(); private final PotConfig potConfig = new PotConfig(); + private final PanConfig panConfig = new PanConfig(); private final SaladHandler saladHandler = new SaladHandler(); private final PieConfig pieConfig = new PieConfig(); private final PieHandler pieHandler = new PieHandler(); @@ -283,6 +285,7 @@ public void loadConfigs() { condimentConfig.load(new File(getDataFolder(), "condiments.yml")); saladConfig.load(new File(getDataFolder(), "salads.yml")); potConfig.load(new File(getDataFolder(), "pot.yml")); + panConfig.load(new File(getDataFolder(), "frying-pan.yml")); pieConfig.load(new File(getDataFolder(), "pies.yml")); popcornMachineConfig.load(new File(getDataFolder(), "popcorn-machines.yml")); millingRecipeLoader.load(new File(getDataFolder(), "milling-recipes.yml")); @@ -452,6 +455,7 @@ public void createConfigs() { "condiments.yml", "salads.yml", "pot.yml", + "frying-pan.yml", "pies.yml", "popcorn-machines.yml", "milling-recipes.yml", diff --git a/src/main/java/net/tfminecraft/cooking/cooking/CookingOutput.java b/src/main/java/net/tfminecraft/cooking/cooking/CookingOutput.java index 641250b..cca91d3 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/CookingOutput.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/CookingOutput.java @@ -14,7 +14,7 @@ /** * A cooking option that names an output (types.yml cooking-options.<method>.output) turns the piece into that - * food once it's done: chopped corn in the frying pan pops into popcorn. The piece cooks raw, then becomes the output, + * food once it's done: chopped corn in the frying pan pops into popcorn, and rice boils into boiled rice in the pot. The piece cooks raw, then becomes the output, * cooked (or burnt, if it burnt first), and keeps cooking by the same clock, so popcorn left too long burns as popcorn. */ public final class CookingOutput { @@ -36,17 +36,26 @@ static void startRaw(FoodItem piece, Method method) { } /** - * A piece taken off before it turned is still what went in: drops the raw track {@link #startRaw} gave it. - * True when the piece is such an unturned input. + * A piece already cooked another way still goes into cookware whose option turns it into its output (boiled rice + * into the frying pan, where it fries into fried rice); burnt, it doesn't. + */ + static boolean turnsHere(FoodItem piece, Method method) { + return output(piece, method) != null && !piece.hasTag(Tag.BURNT); + } + + /** + * A piece taken off before it turned is still what went in: drops the raw track {@link #startRaw} gave it, and a + * piece that came in cooked (boiled rice) keeps its step. True when the piece is such an unturned input. */ static boolean restoreInput(FoodItem piece, Method method) { - if (output(piece, method) == null || piece.getCookData().cooksInPlace()) { + CookParameter parameter = piece.getCookData().getParameters().get(method); + if (parameter == null || parameter.getOutput() == null || piece.getCookData().cooksInPlace() + || piece.getCookData().getCurrentTime() >= parameter.getTime()) { return false; } - if (piece.hasTagTrack(COOKED_TRACK) && !piece.hasTag(Tag.RAW)) { - return false; + if (piece.hasTag(Tag.RAW)) { + piece.removeTrack(COOKED_TRACK); } - piece.removeTrack(COOKED_TRACK); return true; } diff --git a/src/main/java/net/tfminecraft/cooking/cooking/CookingReference.java b/src/main/java/net/tfminecraft/cooking/cooking/CookingReference.java index 3251364..106e1fd 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/CookingReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/CookingReference.java @@ -184,7 +184,7 @@ public void rebuildFromFurniture() { if (fi == null || !fi.canBeCooked() || !fi.getCookData().hasMethod(method)) { return; } - if (!fi.hasTag(Tag.RAW)) { + if (!fi.hasTag(Tag.RAW) && !CookingOutput.turnsHere(fi, method)) { return; } if (!fi.getCookData().isBeingCooked()) { @@ -210,7 +210,7 @@ public void slotAdd(FurnitureSlotItemAddEvent e) { return; } CookingOutput.startRaw(fi, method); - if(!fi.hasTag(Tag.RAW)) { + if(!fi.hasTag(Tag.RAW) && !CookingOutput.turnsHere(fi, method)) { e.setCancelled(true); return; } diff --git a/src/main/java/net/tfminecraft/cooking/cooking/FryingReference.java b/src/main/java/net/tfminecraft/cooking/cooking/FryingReference.java index fc6308d..3edd3ce 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/FryingReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/FryingReference.java @@ -3,22 +3,29 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.TreeSet; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import net.tfminecraft.cooking.cache.ItemCache; import net.tfminecraft.cooking.enums.Method; +import net.tfminecraft.cooking.enums.Tag; import net.tfminecraft.cooking.heat.HeatSources; import net.tfminecraft.cooking.item.FoodItem; import net.tfminecraft.cooking.item.data.CookData; +import net.tfminecraft.cooking.pan.PanConfig; +import net.tfminecraft.cooking.pan.StirIn; import net.tfminecraft.cooking.quality.CompositionContext; import net.tfminecraft.cooking.quality.CompositionApplier; import net.tfminecraft.cooking.quality.CompositionQualityResolver; import net.tfminecraft.cooking.quality.CompositionResult; +import net.tfminecraft.cooking.sandwich.DishBonus; +import net.tfminecraft.cooking.sandwich.SandwichItems; import net.tfminecraft.cooking.utils.IngredientConverter; import net.tfminecraft.cooking.utils.ItemBuilder; import net.tfminecraft.cooking.utils.ItemUpdater; @@ -237,14 +244,21 @@ public void slotRemove(FurnitureSlotItemTakeEvent e) { return; } - if (butterExtra != null) { + List stirred = stirIn(e.getSlot().getId(), fi); + if (butterExtra != null || !stirred.isEmpty()) { List inputs = new ArrayList<>(); inputs.add(fi); - inputs.add(butterExtra); + inputs.addAll(stirred); + if (butterExtra != null) inputs.add(butterExtra); CompositionResult composed = CompositionQualityResolver.compose( e.getPlayer(), inputs, CompositionContext.FRYING_PAN); CompositionApplier.apply(fi, composed); } + if (!stirred.isEmpty()) { + // The dish's stars are set, so a poison it stirred in can hide (frying-pan.yml bonuses). + DishBonus.apply(fi, stirred, food -> null, layer -> false, PanConfig.bonuses()); + item = renamed(item, ItemBuilder.displayName(fi)); + } item = ItemUpdater.applyItemUpdate(item, fi, null); if (item == null) { @@ -259,6 +273,52 @@ public void slotRemove(FurnitureSlotItemTakeEvent e) { new net.tfminecraft.cooking.events.DishCookedEvent(e.getPlayer(), item, "station")); } + /** + * A dish taken out fried (fried rice, frying-pan.yml stir-ins) stirs in the other fried pieces in the pan that it + * takes, up to its max: they leave the pan, their food is added to its own and their origins name it (Egg Fried + * Rice). Pieces still raw, or burnt, stay in the pan. Returns what it stirred in. + */ + private List stirIn(String takenSlot, FoodItem dish) { + List stirred = new ArrayList<>(); + StirIn recipe = PanConfig.stirInFor(dish); + if (recipe == null || !dish.hasTag(Tag.COOKED)) return stirred; + List used = new ArrayList<>(); + for (String slotId : new TreeSet<>(slots.keySet())) { + FoodItem piece = slots.get(slotId); + if (stirred.size() >= recipe.getMax()) break; + if (slotId.equals(takenSlot) || !piece.hasTag(Tag.COOKED) || !recipe.takes(piece)) continue; + stirred.add(piece); + used.add(slotId); + } + if (stirred.isEmpty()) return stirred; + // The parts' food summed as a sandwich's layers are; the dish's own Cooked tag still applies to it when eaten. + double ownFood = SandwichItems.multiplier(dish, true); + double ownNutrition = SandwichItems.multiplier(dish, false); + List parts = new ArrayList<>(); + parts.add(new FoodItem(dish)); + parts.addAll(stirred); + SandwichItems.applyTotals(dish, parts); + dish.setBaseFood(dish.getBaseFood() / ownFood); + dish.setBaseNutrition(dish.getBaseNutrition() / ownNutrition); + for (FoodItem piece : stirred) dish.addIngredient(piece.getOrigin()); + for (String slotId : used) { + slots.remove(slotId); + f.getActiveSlot(slotId).ifPresent(PlacedSlot::clearModel); + f.removeActiveSlot(slotId); + } + return stirred; + } + + // Keep the existing legacy text representation of item names. + @SuppressWarnings("deprecation") + private static ItemStack renamed(ItemStack item, String name) { + ItemStack copy = item.clone(); + ItemMeta meta = copy.getItemMeta(); + meta.setDisplayName(name); + copy.setItemMeta(meta); + return copy; + } + private void clearButterSecondary() { butterExtra = null; if (secondaries.containsKey("butter")) { diff --git a/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java b/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java index eba9fae..3ab5622 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java @@ -305,12 +305,20 @@ private void handleCookingSlots() { if (data.isBeingCooked()) data.stop(); continue; } + // Rice, whose pot option only names an output (boiled rice), is raw once it's in the water. + CookingOutput.startRaw(item, method); if (!data.isBeingCooked()) { data.start(Method.POT); } boolean progressed = data.tick(); if (!progressed) continue; - applySlotUpdate(slot, item); + FoodItem turned = CookingOutput.turn(item, method); + if (turned == item) { + applySlotUpdate(slot, item); + continue; + } + entry.setValue(turned); + applySlotUpdate(slot, turned, ItemBuilder.displayName(turned)); } } @@ -374,6 +382,8 @@ public void take(Player p) { if(slot.getCurrentItem() == null) continue; ItemStack stack = slot.getCurrentItem(); FoodItem fi = slots.get(key); + // Rice taken out before it has boiled is still rice. + CookingOutput.restoreInput(fi, method); if (WarmthUtils.isHeated(fi, 3)) { WarmthUtils.applyHot(fi); } @@ -931,10 +941,14 @@ private void restoreMains() { } } - /** True for food the pot holds as a main: soup pieces and anything that boils. */ + /** + * True for food the pot holds as a main: soup pieces, anything that boils, and what a piece boiled into (boiled + * rice, which only fries from then on). + */ public static boolean keepsOnRebuild(FoodItem fi) { if (fi == null) return false; if (fi.hasTag(Tag.MASHED)) return true; + if (WarmthUtils.isHeated(fi, 3)) return true; return fi.canBeCooked() && fi.getCookData().hasMethod(Method.POT); } diff --git a/src/main/java/net/tfminecraft/cooking/crafting/CraftingRecipe.java b/src/main/java/net/tfminecraft/cooking/crafting/CraftingRecipe.java index ced36c6..332afbe 100644 --- a/src/main/java/net/tfminecraft/cooking/crafting/CraftingRecipe.java +++ b/src/main/java/net/tfminecraft/cooking/crafting/CraftingRecipe.java @@ -24,6 +24,10 @@ public class CraftingRecipe { private boolean shareFood; /** On a station with fill-models, its inputs show as the rising fill. False keeps them as themselves (olives in the cheese press). */ private boolean fill; + /** One of each input line makes a unit (boiled rice and a fish filet roll into sushi), whatever their origins. */ + private boolean combine; + /** The input line (1 = the first) whose origin fills the output's {origin}; 0 for none. */ + private int originFrom; public CraftingRecipe(String id, ConfigurationSection config) { this.id = id; @@ -36,13 +40,20 @@ public CraftingRecipe(String id, ConfigurationSection config) { this.inherit = config.getBoolean("inherit", false); this.shareFood = config.getBoolean("share-food", false); this.fill = config.getBoolean("fill", true); + this.combine = config.getBoolean("combine", false); + this.originFrom = config.getInt("origin-from", 0); } public boolean canAdd(ItemStack item) { - for(String input : inputs) { - if(TLibs.getItemAPI().getChecker().checkItemWithPath(item, input)) return true; + return line(item) >= 0; + } + + /** The input line this item fills (0 = the first), or -1. */ + public int line(ItemStack item) { + for (int i = 0; i < inputs.size(); i++) { + if (TLibs.getItemAPI().getChecker().checkItemWithPath(item, inputs.get(i))) return i; } - return false; + return -1; } public String getId() { @@ -75,4 +86,10 @@ public boolean sharesFood() { public boolean fills() { return fill; } + public boolean combines() { + return combine; + } + public int getOriginFrom() { + return originFrom; + } } diff --git a/src/main/java/net/tfminecraft/cooking/crafting/CraftingStation.java b/src/main/java/net/tfminecraft/cooking/crafting/CraftingStation.java index c35f1c2..81c65fb 100644 --- a/src/main/java/net/tfminecraft/cooking/crafting/CraftingStation.java +++ b/src/main/java/net/tfminecraft/cooking/crafting/CraftingStation.java @@ -1,11 +1,14 @@ package net.tfminecraft.cooking.crafting; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.TreeSet; import java.util.stream.Collectors; import org.bukkit.Sound; @@ -141,7 +144,10 @@ public boolean canAddItem(ItemStack i) { FoodItem fi = FoodItem.fromItem(i); if(fi == null) return true; + // A combining recipe (sushi) mixes its lines' origins: only the items of one line must share theirs. + CraftingRecipe combining = combining(i); for(ItemStack stack : slots.values()) { + if (combining != null && combining.line(stack) != combining.line(i)) continue; FoodItem fs = FoodItem.fromItem(stack); if(fs != null && (fs.getOrigin() == null ? fi.getOrigin() != null : !fs.getOrigin().equalsIgnoreCase(fi.getOrigin()))) { @@ -155,13 +161,22 @@ public void add(ItemStack item, String slotId) { slots.put(slotId, item); if(currentRecipe == null) { - for(CraftingRecipe cr : recipes) { - if(cr.canAdd(item)) { - currentRecipe = cr; - break; - } - } + currentRecipe = recipeFor(item); + } + } + + /** The first recipe that takes this item, or null. */ + private CraftingRecipe recipeFor(ItemStack item) { + for (CraftingRecipe cr : recipes) { + if (cr.canAdd(item)) return cr; } + return null; + } + + /** The combining recipe this item goes into (the station's, or the one it would start), or null. */ + private CraftingRecipe combining(ItemStack item) { + CraftingRecipe recipe = currentRecipe != null ? currentRecipe : recipeFor(item); + return recipe != null && recipe.combines() ? recipe : null; } public void rebuildFromFurniture() { @@ -261,6 +276,10 @@ public void craft(Player p) { cutInPlace(p, recipe); return; } + if (recipe.combines()) { + combine(p, recipe); + return; + } String origin = "none"; List consumedInputs = new ArrayList<>(); @@ -309,6 +328,14 @@ public void craft(Player p) { f.getLoc().getWorld().dropItem(f.getLoc(), poured ? asHeld(remaining) : remaining) .setVelocity(new Vector(Math.random()*0.2-0.1, 0.1, Math.random()*0.2-0.1)); } + place(p, recipe, item, output, displayData); + } + + /** + * Puts a recipe's output on the station in its first free input slot (or drops it when there's none) and picks the + * recipe that output could go on to. + */ + private void place(Player p, CraftingRecipe recipe, FoodItem item, ItemStack output, DisplayData displayData) { currentRecipe = null; f.getLoc().getWorld().playSound(f.getLoc(), Sound.BLOCK_SWEET_BERRY_BUSH_PICK_BERRIES, 1f, 1f); @@ -353,12 +380,63 @@ public void craft(Player p) { } } + /** + * A combining recipe takes one of each input line per unit (sushi: a boiled rice and a fish filet), as many units as + * every line has, and leaves the rest where it lies. The output's {origin} is its origin-from line's (Salmon). + */ + private void combine(Player p, CraftingRecipe recipe) { + int lines = recipe.getInputs().size(); + int[] have = new int[lines]; + for (ItemStack stack : slots.values()) { + int line = recipe.line(stack); + if (line >= 0) have[line] += stack.getAmount(); + } + int units = Arrays.stream(have).min().orElse(0); + if (units == 0) return; + int[] need = new int[lines]; + Arrays.fill(need, units); + String origin = "none"; + List consumed = new ArrayList<>(); + Map taken = new LinkedHashMap<>(); + for (String slotId : new TreeSet<>(slots.keySet())) { + ItemStack source = slots.get(slotId); + int line = recipe.line(source); + if (line < 0 || need[line] == 0) continue; + int take = Math.min(need[line], source.getAmount()); + need[line] -= take; + taken.put(slotId, take); + FoodItem fi = FoodItem.fromItem(source); + for (int i = 0; fi != null && i < take; i++) consumed.add(fi); + if (fi != null && line == recipe.getOriginFrom() - 1) origin = fi.getOrigin(); + } + // Prepare the complete result before taking any ingredient from the board. + Built built = build(p, recipe, consumed, origin); + if (built == null) return; + built.stack().setAmount(units * recipe.getYield()); + var modelData = built.food().getModelData(); + DisplayData displayData = modelData == null ? null : modelData.getDisplayData(f.getId()); + taken.forEach((slotId, take) -> { + ItemStack source = slots.get(slotId); + if (take < source.getAmount()) { + source.setAmount(source.getAmount() - take); + f.getActiveSlot(slotId).ifPresent(slot -> slot.setCurrentItem(source)); + return; + } + f.getActiveSlot(slotId).ifPresent(PlacedSlot::clearModel); + f.removeActiveSlot(slotId); + slots.remove(slotId); + }); + place(p, recipe, built.food(), built.stack(), displayData); + // What's left of one line waits on the board for more of the others. + if (currentRecipe == null && slots.values().stream().anyMatch(recipe::canAdd)) currentRecipe = recipe; + } + private record Built(FoodItem food, ItemStack stack) {} /** The recipe's output made from these inputs, or null when its output line doesn't make an item. */ private Built build(Player p, CraftingRecipe recipe, List inputs, String origin) { String data = recipe.getOutput(); - if(isSingleOrigin() && origin != null && !origin.equalsIgnoreCase("none")) { + if((isSingleOrigin() || recipe.getOriginFrom() > 0) && origin != null && !origin.equalsIgnoreCase("none")) { data = data.replace("{origin}", origin); } diff --git a/src/main/java/net/tfminecraft/cooking/pan/PanConfig.java b/src/main/java/net/tfminecraft/cooking/pan/PanConfig.java new file mode 100644 index 0000000..9cddb95 --- /dev/null +++ b/src/main/java/net/tfminecraft/cooking/pan/PanConfig.java @@ -0,0 +1,47 @@ +package net.tfminecraft.cooking.pan; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.sandwich.DishBonus; + +/** + * frying-pan.yml: the dishes that stir in the other fried pieces in the pan when they're taken out (fried rice), and + * the tags such a dish earns from what it stirred in (bonuses, such as a poison). + */ +public final class PanConfig { + private static List stirIns = List.of(); + private static List bonuses = List.of(); + + public void load(File file) { + YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + List loaded = new ArrayList<>(); + ConfigurationSection section = config.getConfigurationSection("stir-ins"); + if (section != null) { + for (String id : section.getKeys(false)) { + ConfigurationSection stirIn = section.getConfigurationSection(id); + if (stirIn != null) loaded.add(new StirIn(id, stirIn)); + } + } + stirIns = loaded; + bonuses = DishBonus.load(config.getConfigurationSection("bonuses")); + } + + /** The stir-in this food is the dish of, or null. */ + public static StirIn stirInFor(FoodItem dish) { + for (StirIn stirIn : stirIns) { + if (stirIn.isDish(dish)) return stirIn; + } + return null; + } + + /** The tags a dish earns from what it stirred in, written like pot.yml's bonuses (count: origin). */ + public static List bonuses() { + return bonuses; + } +} diff --git a/src/main/java/net/tfminecraft/cooking/pan/StirIn.java b/src/main/java/net/tfminecraft/cooking/pan/StirIn.java new file mode 100644 index 0000000..ca3c4db --- /dev/null +++ b/src/main/java/net/tfminecraft/cooking/pan/StirIn.java @@ -0,0 +1,49 @@ +package net.tfminecraft.cooking.pan; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.configuration.ConfigurationSection; + +import net.tfminecraft.cooking.item.CookingPathHandler; +import net.tfminecraft.cooking.item.FoodItem; + +/** + * A frying-pan.yml stir-in: a dish taken out of the pan fried (fried rice) stirs in the other fried pieces in the pan + * that it takes (a fried egg, cut vegetables, meat), at most max of them. Paths are Cooking food paths. + */ +public final class StirIn { + private final String id; + private final String dish; + private final List takes = new ArrayList<>(); + private final int max; + + public StirIn(String id, ConfigurationSection config) { + this.id = id; + this.dish = config.getString("dish", ""); + this.takes.addAll(config.getStringList("takes")); + this.max = Math.max(1, config.getInt("max", 3)); + } + + public String getId() { + return id; + } + + /** At most this many pieces go into one dish. */ + public int getMax() { + return max; + } + + /** True for the food that stirs the others in (fried rice). */ + public boolean isDish(FoodItem food) { + return CookingPathHandler.matches(food, dish); + } + + /** True for a piece this dish stirs in (a fried egg). */ + public boolean takes(FoodItem piece) { + for (String path : takes) { + if (CookingPathHandler.matches(piece, path)) return true; + } + return false; + } +} diff --git a/src/main/java/net/tfminecraft/cooking/sandwich/SandwichItems.java b/src/main/java/net/tfminecraft/cooking/sandwich/SandwichItems.java index 14ca29b..2d57873 100644 --- a/src/main/java/net/tfminecraft/cooking/sandwich/SandwichItems.java +++ b/src/main/java/net/tfminecraft/cooking/sandwich/SandwichItems.java @@ -90,6 +90,11 @@ public static void applyTotals(FoodItem dish, List layers, boolean bak if (food > 0) dish.setBaseNutrition(nutrition / food); } + /** What a food's lasting tags (Cooked, Burnt) multiply its food (or nutrition) by; its age doesn't count. */ + public static double multiplier(FoodItem food, boolean forFood) { + return kept(food, forFood, false); + } + /** What a layer's lasting tags (burnt bread) do to it; its age becomes the sandwich's own freshness instead. */ private static double kept(FoodItem layer, boolean forFood, boolean bakedTogether) { double multiplier = 1; diff --git a/src/main/resources/conversions.yml b/src/main/resources/conversions.yml index 56ec94e..38ea7de 100644 --- a/src/main/resources/conversions.yml +++ b/src/main/resources/conversions.yml @@ -46,7 +46,7 @@ conversions: - ia.tfmc_cooking:pineapple fruit(type=fruit_2;origin=Pineapple;tags=freshness.0) - ia.tfmc_cooking:cherry fruit(type=fruit_2;origin=Cherry;tags=freshness.0) - ia.tfmc_cooking:cactus_fruit fruit(type=fruit_2;origin=Cactus Fruit;tags=freshness.0) - - ia.tfmc_cooking:rice vegetable(type=vegetable_2;origin=Rice;tags=freshness.0) + - ia.tfmc_cooking:rice grain(type=rice;origin=Rice;tags=freshness.0) - ia.tfmc_cooking:mustard_seeds spice(type=spice_1;origin=Mustard;tags=freshness.0) - ia.tfmc_cooking:cup_of_milk ingredient(type=cup_of_milk;origin=Cow;tags=freshness.0) - v.milk_bucket ingredient(type=milk_bucket;origin=Cow;tags=freshness.0) diff --git a/src/main/resources/crafting-stations.yml b/src/main/resources/crafting-stations.yml index 3aa8ad5..a4584a8 100644 --- a/src/main/resources/crafting-stations.yml +++ b/src/main/resources/crafting-stations.yml @@ -18,6 +18,10 @@ # (Sweet, Cooked, its age) that the output line doesn't set. The station takes one such input at a time. # share-food: true with inherit, each output takes an even share of its input's food (its food over yield) at # its nutrition, so a pie's slices hold what the pie did instead of their type's own food. +# combine: true one of each input line makes a unit, whatever their origins (boiled rice and a fish filet), as +# many units as every line has; the rest stays on the station. On a single-origin station only the inputs of one +# line must share their origin. +# origin-from: the output's {origin} is the origin of what fills input line n (1 = the first). cutting_board: furniture: cutting_board single-origin: true @@ -69,3 +73,12 @@ cutting_board: inputs: - c.seafood(type=seafood_whole) output: seafood(type=seafood_fish_filet;origin={origin};tags=freshness.0) + # A boiled rice and a raw fish filet roll into two pieces of sushi, named for the fish: Salmon Sushi. + sushi: + combine: true + origin-from: 2 + yield: 2 + inputs: + - c.grain(type=boiled_rice;tags=cooked.3) + - c.seafood(type=seafood_fish_filet;tags=cooked.0) + output: meal(type=sushi;origin={origin};tags=freshness.0) diff --git a/src/main/resources/frying-pan.yml b/src/main/resources/frying-pan.yml new file mode 100644 index 0000000..c64cae2 --- /dev/null +++ b/src/main/resources/frying-pan.yml @@ -0,0 +1,35 @@ +# The frying pan (furniture frying_pan): dishes that stir in the other fried pieces in the pan when they're taken out. +# +# Boiled rice fries into fried rice (types.yml boiled_rice output). Fry it beside a fried egg, cut vegetables, +# mushrooms, meat or fish in the same pan and take the rice out once it's fried (Cooked, not burnt): it stirs in the +# other pieces that are fried too (Cooked, not raw or burnt), up to max. Their food is added to the rice's, their +# origins name it (Egg Fried Rice, Beef Fried Rice), and its quality, freshness and variety come from all of them +# (composition.yml context FRYING_PAN, with the pan's butter). Pieces still raw stay in the pan. Take the other piece +# out first to keep it for itself. +# dish: the food taken out that stirs the others in (a Cooking food path) +# takes: what it stirs in, any one of these paths +# max: at most this many pieces (and no more than the pan's other places hold) +stir-ins: + fried_rice: + dish: c.grain(type=fried_rice) + takes: + - c.egg + - c.food(type=egg) + - c.vegetable + - c.meat + - c.seafood + max: 3 + +# Tags such a dish earns from what it stirred in, written like pot.yml's bonuses (count: origin only). +bonuses: + # Red mushrooms poison it; grilled into fried rice of 3 stars or more, nothing shows. + poison: + track: poison + count: origin + origin: Red Mushroom + steps: + 1: tainted + 2: poisoned + 3: deadly + hide-at-stars: 3 + hide-tags: [cooked] diff --git a/src/main/resources/models.yml b/src/main/resources/models.yml index cb22e5b..86392d3 100644 --- a/src/main/resources/models.yml +++ b/src/main/resources/models.yml @@ -3651,6 +3651,187 @@ egg: position: y: 0.05 +# Rice (types.yml rice, boiled_rice, fried_rice, rice_pudding, sushi). Held, rice is its sheaf and the dishes are +# tfmc_pack's bowl and sushi icons; in the pot, the frying pan and on the cutting board rice is a loose heap of grains +# lying flat (the *_heap and rice_grains items), the pan's drawn at 70% just above its floor as popcorn is. +rice: + plain: + weight: 1 + gui: + item: ia.tfmc_cooking:rice + display: + item: ia.tfmc_cooking:rice + furniture: + - pot ia.tfmc_cooking:rice_grains + display-data: + rotation: + x: 90 + rotten: + weight: 3 + tags: + - rotten + gui: + item: ia.tfmc_cooking:rice_rotten + display: + item: ia.tfmc_cooking:rice_rotten + display-data: + rotation: + x: 90 + +boiled_rice: + display-data-furniture: + frying_pan: + rotation: + x: 90 + scale: + x: 0.7 + y: 0.7 + z: 0.7 + position: + y: -0.26 + boiled: + weight: 1 + gui: + item: ia.tfmc_cooking:boiled_rice + display: + item: ia.tfmc_cooking:boiled_rice + furniture: + - pot ia.tfmc_cooking:boiled_rice_heap + - frying_pan ia.tfmc_cooking:boiled_rice_heap + - cutting_board ia.tfmc_cooking:boiled_rice_heap + display-data: + rotation: + x: 90 + burnt: + weight: 2 + tags: + - burnt + gui: + item: ia.tfmc_cooking:boiled_rice_burnt + display: + item: ia.tfmc_cooking:boiled_rice_burnt + furniture: + - pot ia.tfmc_cooking:boiled_rice_heap_burnt + - frying_pan ia.tfmc_cooking:boiled_rice_heap_burnt + - cutting_board ia.tfmc_cooking:boiled_rice_heap_burnt + display-data: + rotation: + x: 90 + rotten: + weight: 3 + tags: + - rotten + gui: + item: ia.tfmc_cooking:boiled_rice_rotten + display: + item: ia.tfmc_cooking:boiled_rice_rotten + furniture: + - pot ia.tfmc_cooking:boiled_rice_heap_rotten + - frying_pan ia.tfmc_cooking:boiled_rice_heap_rotten + - cutting_board ia.tfmc_cooking:boiled_rice_heap_rotten + display-data: + rotation: + x: 90 + +fried_rice: + display-data-furniture: + frying_pan: + rotation: + x: 90 + scale: + x: 0.7 + y: 0.7 + z: 0.7 + position: + y: -0.26 + cooked: + weight: 1 + gui: + item: ia.tfmc_cooking:fried_rice + display: + item: ia.tfmc_cooking:fried_rice + furniture: + - frying_pan ia.tfmc_cooking:fried_rice_heap + display-data: + rotation: + x: 90 + burnt: + weight: 2 + tags: + - burnt + gui: + item: ia.tfmc_cooking:fried_rice_burnt + display: + item: ia.tfmc_cooking:fried_rice_burnt + furniture: + - frying_pan ia.tfmc_cooking:fried_rice_heap_burnt + display-data: + rotation: + x: 90 + rotten: + weight: 3 + tags: + - rotten + gui: + item: ia.tfmc_cooking:fried_rice_rotten + display: + item: ia.tfmc_cooking:fried_rice_rotten + display-data: + rotation: + x: 90 + +rice_pudding: + plain: + weight: 1 + gui: + item: ia.tfmc_cooking:rice_pudding + display: + item: ia.tfmc_cooking:rice_pudding + display-data: + rotation: + x: 90 + y: 0 + z: 90 + position: + y: -0.25 + rotten: + weight: 2 + tags: + - rotten + gui: + item: ia.tfmc_cooking:rice_pudding_rotten + display: + item: ia.tfmc_cooking:rice_pudding_rotten + display-data: + rotation: + x: 90 + y: 0 + z: 90 + position: + y: -0.25 + +sushi: + plain: + weight: 1 + gui: + item: ia.tfmc_cooking:sushi + display: + item: ia.tfmc_cooking:sushi + display-data: + rotation: + x: 90 + rotten: + weight: 2 + tags: + - rotten + gui: + item: ia.tfmc_cooking:sushi_rotten + display: + item: ia.tfmc_cooking:sushi_rotten + display-data: + rotation: + x: 90 + # Pan foods: vanilla mushrooms, grilled mushrooms and popcorn (types.yml mushroom, grilled_mushroom, popcorn). # Their items are flat, so in furniture they're turned to lie down (rotation x 90), as chopped corn is in the pan. brown_mushroom: diff --git a/src/main/resources/pot.yml b/src/main/resources/pot.yml index c9eab2f..3eec938 100644 --- a/src/main/resources/pot.yml +++ b/src/main/resources/pot.yml @@ -45,6 +45,31 @@ mashes: output: mash(type=mashed_potatoes;origin=Potato;tags=freshness.0) display: ia.tfmc_cooking:gray hint: 4 boiled cut potatoes and a butter + # Two boiled rice, a cup of milk and a sweetener (sugar, maple syrup), stirred with the masher: rice pudding. + # Cinnamon, vanilla or nutmeg make it Aromatic. Boiled rice, milk and sugar over a pot of water get the mash hint. + rice_pudding: + ingredients: + rice: + path: c.grain(type=boiled_rice;tags=cooked.3) + amount: 2 + milk: + path: c.ingredient(type=cup_of_milk) + sweetener: + path: c.sweetener + extras: + cinnamon: + path: c.spice(origin=Cinnamon) + track: aromatic + vanilla: + path: c.spice(origin=Vanilla) + track: aromatic + nutmeg: + path: c.spice(origin=Nutmeg) + track: aromatic + servings: 2 + output: mash(type=rice_pudding;origin=Rice;tags=freshness.0) + display: ia.tfmc_cooking:milk + hint: 2 boiled rice, a cup of milk and a sweetener # Named soups: a soup whose boiled pieces make one of these kinds is called by its name instead of its pieces, and # takes the kind as its origin. Rules as in pies.yml / salads.yml kinds (cooking SaladKind), where a piece's layer is diff --git a/src/main/resources/types.yml b/src/main/resources/types.yml index 336f77b..e242da7 100644 --- a/src/main/resources/types.yml +++ b/src/main/resources/types.yml @@ -526,6 +526,78 @@ dough: freshness: rotten: "#8B7355Mouldy" +###Rice — the rice crop (crops.yml rice; conversions.yml makes the harvest this). It boils in the soup pot into +# boiled rice (its pot option's output, as chopped corn pops into popcorn in the pan): a type whose only option has an +# output isn't Raw in hand, only once it's in the water. Taken out before it's done, it's rice again. +rice: + name: "#e8dcb5Rice" + model: rice + food: 8 + nutrition: 5 + cooking-options: + pot: + tag: 3 + time: 15 + burn: 30 + output: grain(type=boiled_rice) + age: + warmth: 450 + freshness: 28 + +###Boiled rice: rice boiled in the pot (Boiled; left in, it burns as boiled rice). It fries into fried rice in the +# frying pan, rolls into sushi with a raw fish filet on the cutting board (crafting-stations.yml sushi) and makes rice +# pudding in an empty pot (pot.yml rice_pudding). +boiled_rice: + name: "#f2eee0Boiled Rice" + model: boiled_rice + food: 10 + nutrition: 5 + cooking-options: + frying_pan: + tag: 1 + time: 15 + burn: 30 + output: grain(type=fried_rice) + age: + warmth: 450 + freshness: 4 + +###Fried rice: boiled rice fried in the frying pan. Taken out, it stirs in the other fried pieces in the pan (a fried +# egg, cut vegetables, mushrooms, meat or fish; frying-pan.yml stir-ins), which name it: Egg Fried Rice. +fried_rice: + name: "#e0b062{fillers}Fried Rice" + model: fried_rice + food: 12 + nutrition: 6 + age: + warmth: 450 + freshness: 4 + +###Rice pudding: two boiled rice, a cup of milk and a sweetener stirred with the masher in an empty pot (pot.yml +# rice_pudding). Its food and nutrition are set from its ingredients; these are only defaults. +rice_pudding: + name: "#f3e6c4Rice Pudding" + model: rice_pudding + food: 20 + nutrition: 6 + age: + warmth: 450 + freshness: 2 + tag-labels: + freshness: + rotten: "#8B7355Spoiled" + +###Sushi: a boiled rice and a raw fish filet rolled on the cutting board, two pieces a pair (crafting-stations.yml +# sushi). The fish names it: Salmon Sushi. +sushi: + name: "#f0e8dc{inherit} Sushi" + model: sushi + food: 10 + nutrition: 7 + age: + warmth: 450 + freshness: 2 + ##SAUCE sauce: age: diff --git a/src/test/java/net/tfminecraft/cooking/cooking/RiceCookingTest.java b/src/test/java/net/tfminecraft/cooking/cooking/RiceCookingTest.java new file mode 100644 index 0000000..5c5ba9d --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/cooking/RiceCookingTest.java @@ -0,0 +1,448 @@ +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.ArrayList; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.configuration.MemoryConfiguration; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import net.tfminecraft.cooking.crafting.CraftingStationCoverageTest; +import net.tfminecraft.cooking.enums.Method; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.item.data.CookParameter; +import net.tfminecraft.cooking.item.model.FoodModel; +import net.tfminecraft.cooking.item.tag.TagTrack; +import net.tfminecraft.cooking.loader.FoodLoader; +import net.tfminecraft.cooking.loader.TrackLoader; +import net.tfminecraft.cooking.pan.PanConfig; +import net.tfminecraft.cooking.pan.StirIn; +import net.tfminecraft.cooking.pot.MashRecipe; +import net.tfminecraft.cooking.pot.PotConfig; +import net.tfminecraft.cooking.quality.CompositionContext; +import net.tfminecraft.cooking.quality.CompositionQualityResolver; +import net.tfminecraft.cooking.sandwich.SandwichItems; +import net.tfminecraft.cooking.utils.FoodParser; +import net.tfminecraft.cooking.utils.ItemBuilder; +import net.tfminecraft.cooking.utils.ItemUpdater; +import net.tfminecraft.interactiblefurniture.events.FurnitureInteractEvent; +import net.tfminecraft.interactiblefurniture.events.FurnitureSlotItemAddEvent; +import net.tfminecraft.interactiblefurniture.events.FurnitureSlotItemTakeEvent; + +/** + * Rice: it boils into boiled rice in the pot (a pot option's output), boiled rice fries into fried rice in the pan and + * stirs in the other fried pieces there (frying-pan.yml), and the shipped config makes rice pudding and sushi of it. + */ +class RiceCookingTest { + static final String BOILED = "grain(type=boiled_rice)"; + static final String FRIED = "grain(type=fried_rice)"; + + static FoodParser.Result parsed(String id, String name, double food, double nutrition) { + MemoryConfiguration config = new MemoryConfiguration(); + config.set("name", name); config.set("update", false); + FoodItem type = new FoodItem(id, config); + type.setModel(new FoodModel(new ItemStack(Material.CARROT))); + FoodParser.Result result = new FoodParser.Result(); + result.template = new FoodItem(type); + result.template.setCategory("grain"); + result.template.setBaseFood(food); + result.template.setBaseNutrition(nutrition); + return result; + } + + static int cooked(FoodItem food) { return food.getTagTrack("cooked").getValue(); } + + static void boiled(FoodItem food) { + TagTrack cooked = new TagTrack(TrackLoader.getByString("cooked")); + cooked.forceSetValue(3); + food.addOrModifyTrack(cooked); + } + + /** The soup pot boils rice into boiled rice. */ + @Nested + class Pot { + final CookingReferencesTest.Environment env = new CookingReferencesTest.Environment(); + final PotReference pot = new PotReference(env.furniture, Method.POT); + + @AfterEach void close() { env.close(); } + + Pot() { + env.parser.when(() -> FoodParser.parse(BOILED)).thenAnswer(inv -> parsed("boiled_rice", "Boiled Rice", 10, 5)); + env.builder.when(() -> ItemBuilder.displayName(any())).thenAnswer(inv -> ((FoodItem) inv.getArgument(0)).getName()); + } + + /** Rice as conversions.yml makes it: its only option (the pot) names an output, so it carries no Raw tag. */ + ItemStack rice() { + FoodItem rice = env.foodNamed("rice", "grain", "Rice", "Rice"); + rice.getCookData().getParameters().put(Method.POT, new CookParameter(3, 3, 6, BOILED)); + FoodItem copy = new FoodItem(rice); + assertFalse(copy.hasTagTrack("cooked")); + return env.stack(copy, Material.WHEAT_SEEDS, 2); + } + + void hold(ItemStack item) { + env.player.getInventory().setItemInMainHand(item); + FurnitureInteractEvent event = env.interaction(); + pot.interact(event); + } + + void boil() { + hold(new ItemStack(Material.WATER_BUCKET)); + env.heated = true; + for (int i = 0; i < 20; i++) pot.tick(); + assertTrue(pot.isBoiling()); + } + + ItemStack hand() { return env.player.getInventory().getItemInMainHand(); } + + @Test + void riceBoilsIntoBoiledRiceThenBurnsAsBoiledRiceAndComesOutHot() { + boil(); + hold(rice()); + FoodItem rice = pot.getSlot("input_1"); + assertEquals("rice", rice.getId()); + pot.tick(); + assertEquals(0, cooked(pot.getSlot("input_1")), "Raw once it's in the water"); + pot.tick(); + pot.tick(); + FoodItem boiled = pot.getSlot("input_1"); + assertEquals("boiled_rice", boiled.getId()); + assertEquals("Rice", boiled.getOrigin()); + assertEquals(3, cooked(boiled)); + env.updater.verify(() -> ItemUpdater.applyItemUpdate( + argThat(stack -> stack != null && "Boiled Rice".equals(stack.getItemMeta().getDisplayName())), + same(boiled), any())); + assertNull(boiled.getCookData().getParameters().get(Method.POT).getOutput()); + for (int i = 0; i < 3; i++) pot.tick(); + assertSame(boiled, pot.getSlot("input_1"), "Burnt boiled rice stays boiled rice"); + assertEquals(2, cooked(boiled)); + + hold(new ItemStack(Material.AIR)); + FoodItem taken = env.resolve(hand()); + assertEquals("boiled_rice", taken.getId()); + assertTrue(pot.getSlots().isEmpty()); + } + + @Test + void riceTakenOutBeforeItHasBoiledIsRiceAgain() { + boil(); + hold(rice()); + pot.tick(); + hold(new ItemStack(Material.AIR)); + FoodItem taken = env.resolve(hand()); + assertEquals("rice", taken.getId()); + assertFalse(taken.hasTagTrack("cooked"), "No Raw tag on rice in hand"); + assertFalse(taken.hasTagTrack("warmth")); + } + + @Test + void aRebuiltPotKeepsItsBoiledRiceThoughBoiledRiceOnlyFries() { + FoodItem boiled = env.foodNamed("boiled_rice", "grain", "Rice", "Boiled Rice"); + boiled.getCookData().getParameters().put(Method.FRYING_PAN, new CookParameter(1, 15, 30, FRIED)); + boiled(boiled); + assertTrue(PotReference.keepsOnRebuild(boiled)); + assertFalse(PotReference.keepsOnRebuild(env.foodNamed("butter", "dairy", "Cow", "Butter"))); + } + } + + /** Boiled rice fries into fried rice, which stirs in the other fried pieces in the pan. */ + @Nested + class Pan { + final CraftingStationCoverageTest.Environment env = new CraftingStationCoverageTest.Environment(); + final FryingReference pan = new FryingReference(env.furniture, Method.FRYING_PAN); + @TempDir Path directory; + + @AfterEach void close() { + new PanConfig().load(new File("missing-frying-pan.yml")); + env.close(); + } + + Pan() { + new PanConfig().load(new File("src/main/resources/frying-pan.yml")); + env.parser.when(() -> FoodParser.parse(FRIED)).thenAnswer(inv -> parsed("fried_rice", "{fillers}Fried Rice", 12, 6)); + env.builder.when(() -> ItemBuilder.displayName(any())).thenAnswer(inv -> { + FoodItem food = inv.getArgument(0); + return String.join(" and ", food.getIngredients()) + (food.getIngredients().isEmpty() ? "" : " ") + + food.getName().replace("{fillers}", ""); + }); + } + + FoodItem boiledRice(int time, int burn) { + FoodItem rice = env.food("boiled_rice", "Rice"); + rice.setCategory("grain"); + rice.setBaseFood(10); + rice.setBaseNutrition(5); + rice.getCookData().getParameters().put(Method.FRYING_PAN, new CookParameter(1, time, burn, FRIED)); + boiled(rice); + return rice; + } + + /** A piece that fries as itself (an egg, a steak): Raw, cooked at tick 2, burnt at tick 20. */ + FoodItem piece(String id, String category, String origin, double food, double nutrition) { + FoodItem piece = env.food(id, origin, Method.FRYING_PAN); + piece.setCategory(category); + piece.setBaseFood(food); + piece.setBaseNutrition(nutrition); + return piece; + } + + void add(FoodItem food, String slot) { + FurnitureSlotItemAddEvent event = env.addEvent(slot, env.stack(food, Material.WHEAT, 1)); + pan.slotAdd(event); + assertFalse(event.isCancelled(), food.getId()); + env.place(slot, event.getItem()); + } + + FurnitureSlotItemTakeEvent take(String slot) { + FurnitureSlotItemTakeEvent event = env.takeEvent(slot, env.active.get(slot).getCurrentItem()); + pan.slotRemove(event); + return event; + } + + void tick(int times) { for (int i = 0; i < times; i++) pan.tick(); } + + @Test + void boiledRiceFriesIntoFriedRiceThatStirsInAFriedEggAndIsNamedForIt() { + pan.getSecondaries().put("butter", -1); + FoodItem egg = piece("egg", "egg", "Egg", 10, 6); + add(egg, "input_2"); + add(boiledRice(5, 30), "input_1"); + tick(5); + FoodItem fried = pan.getSlot("input_1"); + assertEquals("fried_rice", fried.getId()); + assertEquals("Rice", fried.getOrigin()); + assertEquals(1, cooked(fried)); + assertEquals(1, cooked(egg)); + + FurnitureSlotItemTakeEvent taken = take("input_1"); + assertFalse(taken.isCancelled()); + FoodItem dish = env.resolve(taken.getItem()); + assertSame(fried, dish); + assertEquals(List.of("Egg"), dish.getIngredients()); + env.updater.verify(() -> ItemUpdater.applyItemUpdate( + argThat(stack -> stack != null && "Egg Fried Rice".equals(stack.getItemMeta().getDisplayName())), + same(dish), isNull())); + // Its food with its own Cooked tag is the rice's and the egg's, each with theirs (12 x 1.55 + 10 x 1.55). + assertEquals(22, dish.getBaseFood(), 1e-9); + assertEquals(6, dish.getBaseNutrition(), 1e-9); + assertNull(pan.getSlot("input_2"), "The egg went into the rice"); + assertFalse(env.active.containsKey("input_2")); + env.composer.verify(() -> CompositionQualityResolver.compose(any(), + argThat(inputs -> inputs.size() == 2), eq(CompositionContext.FRYING_PAN))); + assertNull(dish.getTagTrack("poison")); + assertTrue(dish.hasTagTrack("warmth"), "Served hot"); + } + + @Test + void piecesStillRawStayInThePanAndPlainFriedRiceNeedsNoButter() { + FoodItem steak = env.food("steak", "Beef"); + steak.setCategory("meat"); + steak.addOrModifyTrack(TrackLoader.getByString("cooked")); + steak.getCookData().getParameters().put(Method.FRYING_PAN, new CookParameter(1, 50, 60)); + add(new FoodItem(steak), "input_2"); + add(boiledRice(5, 30), "input_1"); + tick(5); + FurnitureSlotItemTakeEvent taken = take("input_1"); + FoodItem dish = env.resolve(taken.getItem()); + assertTrue(dish.getIngredients().isEmpty()); + assertEquals(12, dish.getBaseFood(), 1e-9); + assertNotNull(pan.getSlot("input_2"), "The raw steak stays"); + env.composer.verifyNoInteractions(); + } + + @Test + void burntFriedRiceStirsInNothingAndBurntBoiledRiceStaysOutOfThePan() { + FoodItem egg = piece("egg", "egg", "Egg", 10, 6); + egg.getCookData().getParameters().put(Method.FRYING_PAN, new CookParameter(1, 2, 200)); + add(egg, "input_2"); + add(boiledRice(5, 7), "input_1"); + tick(7); + assertEquals(2, cooked(pan.getSlot("input_1"))); + FoodItem dish = env.resolve(take("input_1").getItem()); + assertTrue(dish.getIngredients().isEmpty()); + assertNotNull(pan.getSlot("input_2")); + + FoodItem burnt = boiledRice(2, 4); + burnt.getTagTrack("cooked").forceSetValue(2); + FurnitureSlotItemAddEvent refused = env.addEvent("input_3", env.stack(burnt, Material.WHEAT, 1)); + pan.slotAdd(refused); + assertTrue(refused.isCancelled()); + } + + @Test + void boiledRiceTakenOffBeforeItFriesComesBackBoiled() { + pan.getSecondaries().put("butter", -1); + FoodItem rice = boiledRice(10, 20); + add(rice, "input_1"); + tick(6); + FurnitureSlotItemTakeEvent taken = take("input_1"); + assertFalse(taken.isCancelled()); + assertSame(rice, env.resolve(taken.getItem())); + assertEquals(3, cooked(rice), "Still boiled"); + assertFalse(rice.hasTagTrack("warmth")); + assertTrue(pan.getSecondaries().containsKey("butter"), "The butter is still in the pan"); + } + + @Test + void aRedMushroomStirredInPoisonsItAndAtMostMaxPiecesGoIn() throws Exception { + Path file = directory.resolve("frying-pan.yml"); + Files.writeString(file, Files.readString(Path.of("src/main/resources/frying-pan.yml")).replace("max: 3", "max: 1")); + new PanConfig().load(file.toFile()); + FoodItem mushroom = piece("grilled_mushroom", "vegetable", "Red Mushroom", 6, 4.5); + FoodItem carrot = piece("vegetable_cut", "vegetable", "Carrot", 7, 4.5); + add(mushroom, "input_2"); + add(carrot, "input_3"); + add(boiledRice(5, 30), "input_1"); + tick(5); + FoodItem dish = env.resolve(take("input_1").getItem()); + assertEquals(List.of("Red Mushroom"), dish.getIngredients()); + assertEquals("tainted", dish.getTagTrack("poison").getCurrentStep().getId()); + assertNotNull(pan.getSlot("input_3"), "Max 1: the carrot stays"); + } + + @Test + void aRebuiltPanKeepsFryingItsBoiledRice() { + FoodItem rice = boiledRice(3, 30); + env.place("input_1", env.stack(rice, Material.WHEAT, 1)); + FoodItem fried = new FoodItem(rice); + fried.getTagTrack("cooked").forceSetValue(1); + fried.getCookData().getParameters().put(Method.FRYING_PAN, new CookParameter(1, 3, 30)); + env.place("input_2", env.stack(fried, Material.WHEAT, 1)); + pan.rebuildFromFurniture(); + assertSame(rice, pan.getSlot("input_1")); + assertNull(pan.getSlot("input_2"), "Done pieces aren't cooking any more"); + } + + @Test + void stirInsMatchTheirDishAndPiecesByPath() { + StirIn stirIn = PanConfig.stirInFor(parsed("fried_rice", "Fried Rice", 12, 6).template); + assertEquals("fried_rice", stirIn.getId()); + assertEquals(3, stirIn.getMax()); + FoodItem egg = piece("egg", "egg", "Egg", 1, 1); + assertTrue(stirIn.takes(egg)); + assertFalse(stirIn.takes(piece("popcorn", "snack", "Corn", 1, 1))); + assertNull(PanConfig.stirInFor(egg)); + assertEquals(1, PanConfig.bonuses().size()); + MemoryConfiguration bare = new MemoryConfiguration(); + StirIn defaults = new StirIn("bare", bare); + assertEquals(3, defaults.getMax()); + assertFalse(defaults.isDish(egg)); + } + + @Test + void aMissingFileOrStirInsSectionLeavesNoneAndSkipsStrayEntries() throws Exception { + new PanConfig().load(directory.resolve("missing.yml").toFile()); + assertNull(PanConfig.stirInFor(parsed("fried_rice", "Fried Rice", 12, 6).template)); + assertTrue(PanConfig.bonuses().isEmpty()); + Path file = directory.resolve("frying-pan.yml"); + Files.writeString(file, "stir-ins:\n stray: 3\n"); + new PanConfig().load(file.toFile()); + assertNull(PanConfig.stirInFor(parsed("fried_rice", "Fried Rice", 12, 6).template)); + } + } + + /** The shipped types, pot and models give rice its whole journey. */ + @Nested + class ShippedConfig { + final CookingReferencesTest.Environment env = new CookingReferencesTest.Environment(); + List previous = FoodLoader.oList; + + ShippedConfig() { + // Real path matching: the environment's parser mock otherwise matches paths by category only. + env.parser.when(() -> FoodParser.extractFields(anyString())).thenCallRealMethod(); + } + + @AfterEach void close() { + FoodLoader.oList = previous; + new PotConfig().load(new File("missing-pot.yml")); + env.close(); + } + + FoodItem type(String id) { + return new FoodItem(FoodLoader.getByString(id)); + } + + @Test + void riceBoilsInThePotAndBoiledRiceFriesInThePanAndNeitherIsRawInHand() throws Exception { + FoodLoader.oList = new ArrayList<>(); + new FoodLoader().load(new File("src/main/resources/types.yml")); + FoodItem rice = type("rice"); + assertEquals(BOILED, rice.getCookData().getParameters().get(Method.POT).getOutput()); + assertFalse(rice.hasTagTrack("cooked")); + FoodItem boiled = type("boiled_rice"); + assertEquals(FRIED, boiled.getCookData().getParameters().get(Method.FRYING_PAN).getOutput()); + assertFalse(boiled.hasTagTrack("cooked")); + for (String id : List.of("fried_rice", "rice_pudding", "sushi")) { + assertTrue(type(id).getCookData().getParameters().isEmpty(), id); + } + assertTrue(Files.readString(Path.of("src/main/resources/conversions.yml")) + .contains("ia.tfmc_cooking:rice grain(type=rice;origin=Rice;tags=freshness.0)")); + assertEquals(1.55, SandwichItems.multiplier(cookedRice(boiled), true), 1e-9); + } + + FoodItem cookedRice(FoodItem boiled) { + boiled(boiled); + boiled.getTagTrack("cooked").forceSetValue(1); + return boiled; + } + + @Test + void riceObjectsHaveModelsAndItemsAdderItems() throws Exception { + String models = Files.readString(Path.of("src/main/resources/models.yml")); + StringBuilder items = new StringBuilder(); + for (String file : List.of("intermediates.yml", "categories.yml")) { + items.append(Files.readString(Path.of("ItemsAdder/tfmc_cooking/contents/" + file))); + } + for (String model : List.of("rice", "boiled_rice", "fried_rice", "rice_pudding", "sushi")) { + assertTrue(models.contains("\n" + model + ":\n"), model); + } + for (String id : List.of("rice_rotten", "rice_grains", "boiled_rice", "boiled_rice_burnt", "boiled_rice_rotten", + "boiled_rice_heap", "boiled_rice_heap_burnt", "boiled_rice_heap_rotten", "fried_rice", "fried_rice_burnt", + "fried_rice_rotten", "fried_rice_heap", "fried_rice_heap_burnt", "rice_pudding", "rice_pudding_rotten", + "sushi", "sushi_rotten")) { + assertTrue(models.contains("ia.tfmc_cooking:" + id + "\n"), id); + assertTrue(items.toString().contains("\n " + id + ":\n"), id); + assertTrue(items.toString().contains("- tfmc_cooking:" + id + "\n"), id); + assertTrue(Files.exists(Path.of("ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/" + + id + ".png")), id); + } + } + + @Test + void anEmptyPotStirsTwoBoiledRiceAMilkAndASweetenerIntoRicePudding() { + new PotConfig().load(new File("src/main/resources/pot.yml")); + MashRecipe pudding = PotConfig.mash("rice_pudding"); + assertEquals(2, pudding.getServings()); + assertEquals("mash(type=rice_pudding;origin=Rice;tags=freshness.0)", pudding.getOutput()); + FoodItem rice = env.foodNamed("boiled_rice", "grain", "Rice", "Boiled Rice"); + boiled(rice); + FoodItem burnt = env.foodNamed("boiled_rice", "grain", "Rice", "Boiled Rice"); + boiled(burnt); + burnt.getTagTrack("cooked").forceSetValue(2); + FoodItem milk = env.foodNamed("cup_of_milk", "ingredient", "Cow", "Cup of Milk"); + FoodItem sugar = env.foodNamed("sweetener_1", "sweetener", "Sugar", "Sugar"); + FoodItem cinnamon = env.foodNamed("spice_1", "spice", "Cinnamon", "Cinnamon"); + assertTrue(pudding.takesIngredient(rice, List.of())); + assertFalse(pudding.takesIngredient(burnt, List.of()), "Burnt rice makes no pudding"); + assertTrue(pudding.takesIngredient(milk, List.of(rice, rice))); + assertFalse(pudding.complete(List.of(rice, rice, milk))); + assertTrue(pudding.complete(List.of(rice, milk, rice, sugar))); + assertEquals("aromatic", pudding.extraFor(cinnamon).track()); + assertTrue(PotConfig.isMashIngredient(sugar), "Sugar over a pot of water gets the mash hint"); + // Milk alone starts a pudding, not mashed potatoes, whose milk is only an extra. + assertFalse(PotConfig.mash("mashed_potatoes").holds(List.of(milk), List.of())); + assertTrue(pudding.holds(List.of(milk), List.of())); + } + } +} diff --git a/src/test/java/net/tfminecraft/cooking/crafting/CombineRecipeTest.java b/src/test/java/net/tfminecraft/cooking/crafting/CombineRecipeTest.java new file mode 100644 index 0000000..685ed6c --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/crafting/CombineRecipeTest.java @@ -0,0 +1,169 @@ +package net.tfminecraft.cooking.crafting; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemoryConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +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.events.DishCookedEvent; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.quality.CompositionContext; +import net.tfminecraft.cooking.quality.CompositionQualityResolver; +import net.tfminecraft.cooking.utils.FoodParser; +import net.tfminecraft.interactiblefurniture.furniture.PlacedSlot; + +/** A combining recipe: one of each input line per unit, whatever their origins (boiled rice and a fish filet: sushi). */ +class CombineRecipeTest { + CraftingStationCoverageTest.Environment env; + + @BeforeEach void open() { env = new CraftingStationCoverageTest.Environment(); } + @AfterEach void close() { env.close(); } + + /** A single-origin board (as the cutting board is) with the sushi recipe; the test checker matches food:. */ + CraftingStation board() { + MemoryConfiguration config = env.config(2, true, "tool:knife"); + config.set("recipes.sushi.combine", true); + config.set("recipes.sushi.origin-from", 2); + config.set("recipes.sushi.yield", 2); + config.set("recipes.sushi.inputs", List.of("food:boiled_rice", "food:filet")); + config.set("recipes.sushi.output", "meal(type=sushi;origin={origin})"); + return new CraftingStation(env.furniture, new CraftingStation("board", config)); + } + + ItemStack rice(int amount) { return env.stack(env.food("boiled_rice", "Rice"), Material.WHITE_WOOL, amount); } + ItemStack filet(String fish, int amount) { return env.stack(env.food("filet", fish), Material.COD, amount); } + + @Test + void recipeReadsCombineAndOriginFromAndFindsTheLineAnItemFills() { + CraftingRecipe plain = new CraftingRecipe("plain", new MemoryConfiguration()); + assertFalse(plain.combines()); + assertEquals(0, plain.getOriginFrom()); + assertEquals(-1, plain.line(rice(1))); + CraftingRecipe sushi = recipe(); + assertTrue(sushi.combines()); + assertEquals(2, sushi.getOriginFrom()); + assertEquals(0, sushi.line(rice(1))); + assertEquals(1, sushi.line(filet("Salmon", 1))); + assertEquals(-1, sushi.line(env.raw(1))); + } + + CraftingRecipe recipe() { + MemoryConfiguration config = new MemoryConfiguration(); + config.set("combine", true); + config.set("origin-from", 2); + config.set("inputs", List.of("food:boiled_rice", "food:filet")); + return new CraftingRecipe("sushi", config); + } + + @Test + void riceAndAFiletOfAnotherOriginRollIntoTwoPiecesNamedForTheFish() { + CraftingStation board = board(); + env.add(board, "input_1", rice(1)); + ItemStack salmon = filet("Salmon", 1); + assertTrue(board.canAddItem(salmon), "A single-origin board still takes the fish beside the rice"); + env.add(board, "input_2", salmon); + + board.craft(env.player); + assertEquals("meal(type=sushi;origin=Salmon)", env.lastParsed); + ItemStack sushi = board.getSlots().get("input_1"); + assertEquals(2, sushi.getAmount(), "Two pieces a pair"); + assertEquals("meal(type=sushi;origin=Salmon)", env.resolve(sushi).getId()); + assertNull(board.getSlots().get("input_2")); + assertFalse(env.active.containsKey("input_2")); + assertFalse(board.hasRecipe(), "Nothing left to roll"); + assertTrue(env.dropped.isEmpty()); + env.composer.verify(() -> CompositionQualityResolver.compose(same(env.player), + argThat(inputs -> inputs.size() == 2), eq(CompositionContext.CUTTING_BOARD))); + assertEquals(1, env.server.getPluginManager().getFiredEvents().filter(e -> e instanceof DishCookedEvent).count()); + + // With the sushi still on the board, more rice starts the recipe again: the sushi's origin doesn't count. + assertTrue(board.canAddItem(rice(1))); + } + + @Test + void asManyPiecesAsEveryLineHasAndTheRestWaitsForMore() { + CraftingStation board = board(); + PlacedSlot riceSlot = env.add(board, "input_1", rice(3)); + env.add(board, "input_2", filet("Salmon", 1)); + board.craft(env.player); + ItemStack left = board.getSlots().get("input_1"); + assertEquals(2, left.getAmount(), "One rice was used"); + verify(riceSlot).setCurrentItem(left); + assertEquals(2, board.getSlots().get("input_2").getAmount(), "The sushi took the filet's place"); + assertTrue(board.hasRecipe(), "The rice left over waits for another filet"); + + assertTrue(board.canAddItem(filet("Cod", 1)), "The salmon is used up, so another fish may come"); + env.add(board, "input_3", filet("Cod", 2)); + assertFalse(board.canAddItem(filet("Salmon", 1)), "One fish at a time"); + assertTrue(board.canAddItem(rice(1))); + board.craft(env.player); + assertEquals("meal(type=sushi;origin=Cod)", env.lastParsed); + assertNull(board.getSlots().get("input_3"), "Both cod filets used"); + assertEquals(4, board.getSlots().get("input_1").getAmount(), "Two pairs, four pieces, where the rice was"); + assertFalse(board.hasRecipe()); + } + + @Test + void oneLineAloneMakesNothingAndAnOutputThatDoesNotParseTakesNothing() { + CraftingStation board = board(); + ItemStack lonely = rice(2); + env.add(board, "input_1", lonely); + board.craft(env.player); + assertSame(lonely, board.getSlots().get("input_1")); + assertEquals(2, lonely.getAmount()); + + env.add(board, "input_2", filet("Salmon", 1)); + env.parser.when(() -> FoodParser.parse(anyString())).thenReturn(null); + board.craft(env.player); + assertEquals(2, lonely.getAmount(), "Nothing is used when the sushi can't be made"); + assertNotNull(board.getSlots().get("input_2")); + } + + @Test + void aFiletWithoutFoodDataFillsItsLineButNamesNothing() { + MemoryConfiguration config = env.config(1, false, "tool:knife"); + config.set("recipes", null); + config.set("recipes.mix.combine", true); + config.set("recipes.mix.inputs", List.of("food:boiled_rice", "v.stone")); + config.set("recipes.mix.output", "meal(type=mix;origin={origin})"); + CraftingStation station = new CraftingStation(env.furniture, new CraftingStation("board", config)); + env.add(station, "input_1", rice(1)); + env.add(station, "input_2", new ItemStack(Material.STONE)); + station.craft(env.player); + assertEquals("meal(type=mix;origin={origin})", env.lastParsed, "No origin-from, no origin"); + assertEquals(1, station.getSlots().size()); + } + + @Test + void theShippedCuttingBoardRollsSushiFromBoiledRiceAndARawFilet() { + YamlConfiguration stations = YamlConfiguration.loadConfiguration(new File("src/main/resources/crafting-stations.yml")); + ConfigurationSection sushi = stations.getConfigurationSection("cutting_board.recipes.sushi"); + CraftingRecipe recipe = new CraftingRecipe("sushi", sushi); + assertTrue(recipe.combines()); + assertEquals(2, recipe.getOriginFrom()); + assertEquals(2, recipe.getYield()); + assertEquals(List.of("c.grain(type=boiled_rice;tags=cooked.3)", "c.seafood(type=seafood_fish_filet;tags=cooked.0)"), + recipe.getInputs()); + assertEquals("meal(type=sushi;origin={origin};tags=freshness.0)", recipe.getOutput()); + assertTrue(stations.getBoolean("cutting_board.single-origin")); + } + + @Test + void anItemOutsideTheRecipeOnASingleOriginBoardIsStillComparedByOrigin() { + CraftingStation board = board(); + env.add(board, "input_1", rice(1)); + FoodItem other = env.food("boiled_rice", "Wild Rice"); + assertFalse(board.canAddItem(env.stack(other, Material.WHITE_WOOL, 1)), "Rice of another origin on the rice line"); + } +}