From ab32d6b0a35fb1e4b3ca85ed3198d09fccbf4b00 Mon Sep 17 00:00:00 2001 From: Justinas Launikonis <95593194+JustinasLa@users.noreply.github.com> Date: Sun, 1 Nov 2026 21:00:17 +0200 Subject: [PATCH] feat: cook rice into boiled rice, fried rice, rice pudding and sushi Harvested rice is now its own grain type (it was a vegetable_2 with the tomato model, which the board cut into "Rice (Cut)"). It boils in the soup pot into boiled rice: the pot now honours a cooking option's output like the frying pan. Boiled rice goes into the pan although it isn't raw and fries into fried rice, which stirs in the other fried pieces beside it when it's taken out (frying-pan.yml stir-ins: Egg Fried Rice, with a poison bonus like the other dishes). Two boiled rice, a cup of milk and a sweetener make rice pudding as a pot.yml mash. A boiled rice and a raw fish filet roll into two pieces of sushi on the cutting board through a new combine recipe (one of each input, mixed origins, origin-from names it Salmon Sushi). New tfmc_cooking items reuse tfmc_pack's rice bowl, rice soup and sushi icons. Co-Authored-By: Claude Opus 5.5 --- .../tfmc_cooking/contents/categories.yml | 17 + .../tfmc_cooking/contents/intermediates.yml | 119 +++++ .../models/item/intermediate/boiled_rice.json | 6 + .../item/intermediate/boiled_rice_burnt.json | 6 + .../item/intermediate/boiled_rice_heap.json | 6 + .../intermediate/boiled_rice_heap_burnt.json | 6 + .../intermediate/boiled_rice_heap_rotten.json | 6 + .../item/intermediate/boiled_rice_rotten.json | 6 + .../models/item/intermediate/fried_rice.json | 6 + .../item/intermediate/fried_rice_burnt.json | 6 + .../item/intermediate/fried_rice_heap.json | 6 + .../intermediate/fried_rice_heap_burnt.json | 6 + .../item/intermediate/fried_rice_rotten.json | 6 + .../models/item/intermediate/rice_grains.json | 6 + .../item/intermediate/rice_pudding.json | 6 + .../intermediate/rice_pudding_rotten.json | 6 + .../models/item/intermediate/rice_rotten.json | 6 + .../models/item/intermediate/sushi.json | 6 + .../item/intermediate/sushi_rotten.json | 6 + .../item/intermediate/boiled_rice.png | Bin 0 -> 271 bytes .../item/intermediate/boiled_rice_burnt.png | Bin 0 -> 270 bytes .../item/intermediate/boiled_rice_heap.png | Bin 0 -> 188 bytes .../intermediate/boiled_rice_heap_burnt.png | Bin 0 -> 196 bytes .../intermediate/boiled_rice_heap_rotten.png | Bin 0 -> 188 bytes .../item/intermediate/boiled_rice_rotten.png | Bin 0 -> 273 bytes .../textures/item/intermediate/fried_rice.png | Bin 0 -> 277 bytes .../item/intermediate/fried_rice_burnt.png | Bin 0 -> 270 bytes .../item/intermediate/fried_rice_heap.png | Bin 0 -> 200 bytes .../intermediate/fried_rice_heap_burnt.png | Bin 0 -> 216 bytes .../item/intermediate/fried_rice_rotten.png | Bin 0 -> 277 bytes .../item/intermediate/rice_grains.png | Bin 0 -> 185 bytes .../item/intermediate/rice_pudding.png | Bin 0 -> 203 bytes .../item/intermediate/rice_pudding_rotten.png | Bin 0 -> 203 bytes .../item/intermediate/rice_rotten.png | Bin 0 -> 193 bytes .../textures/item/intermediate/sushi.png | Bin 0 -> 336 bytes .../item/intermediate/sushi_rotten.png | Bin 0 -> 341 bytes README.md | 1 + .../java/net/tfminecraft/cooking/Cooking.java | 4 + .../cooking/cooking/CookingOutput.java | 23 +- .../cooking/cooking/CookingReference.java | 4 +- .../cooking/cooking/FryingReference.java | 64 ++- .../cooking/cooking/PotReference.java | 18 +- .../cooking/crafting/CraftingRecipe.java | 23 +- .../cooking/crafting/CraftingStation.java | 92 +++- .../tfminecraft/cooking/pan/PanConfig.java | 47 ++ .../net/tfminecraft/cooking/pan/StirIn.java | 49 ++ .../cooking/sandwich/SandwichItems.java | 5 + src/main/resources/conversions.yml | 2 +- src/main/resources/crafting-stations.yml | 13 + src/main/resources/frying-pan.yml | 35 ++ src/main/resources/models.yml | 181 +++++++ src/main/resources/pot.yml | 25 + src/main/resources/types.yml | 72 +++ .../cooking/cooking/RiceCookingTest.java | 448 ++++++++++++++++++ .../cooking/crafting/CombineRecipeTest.java | 169 +++++++ 55 files changed, 1489 insertions(+), 24 deletions(-) create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_burnt.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap_burnt.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_heap_rotten.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/boiled_rice_rotten.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_burnt.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_heap.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_heap_burnt.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/fried_rice_rotten.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_grains.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_pudding.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_pudding_rotten.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/rice_rotten.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sushi.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sushi_rotten.json create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_burnt.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap_burnt.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_heap_rotten.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/boiled_rice_rotten.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_burnt.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_heap.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_heap_burnt.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/fried_rice_rotten.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_grains.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_pudding.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_pudding_rotten.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/rice_rotten.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sushi.png create mode 100644 ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sushi_rotten.png create mode 100644 src/main/java/net/tfminecraft/cooking/pan/PanConfig.java create mode 100644 src/main/java/net/tfminecraft/cooking/pan/StirIn.java create mode 100644 src/main/resources/frying-pan.yml create mode 100644 src/test/java/net/tfminecraft/cooking/cooking/RiceCookingTest.java create mode 100644 src/test/java/net/tfminecraft/cooking/crafting/CombineRecipeTest.java 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 0000000000000000000000000000000000000000..20b62c395453ba15b299ae2a8099992e7e303929 GIT binary patch literal 271 zcmV+q0r38bP)p!v!V20pzjj66QSi$q> z&*6r^Tmj?5U^75R@PD$m%>O`h z_W!n8tp6?5m@%-IG4FqWbMF7D*jxYtM#?PzvjfEc=le_i&k2NKF({V!A7ak-UtO31 ze_(=~r!2zIn5FeciKtohSAy`?I1dT%v000fx V!fyiwl-K|O002ovPDHLkV1nbme5e2b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..da080aa4f0c2625df0da67196b52465c9390aad0 GIT binary patch literal 270 zcmV+p0rCEcP)b6@1A+v$8S3TR0ncnbA=HBQG^-WZE5z>R zV5n$K>@CiNPDCaGaNLUS_bh7&MWgnrGp^Q@%p#;@yh)Mez4@0Yq|p UW2Kn;+}h&#e`4k$ z(}N%OR+|T8G_f^`{Q3X?`TutQ{09;2~%v{Y)z7o`WI7GQpOKx$fWi;+PZ8xtOusd}&}7#ylaGakH$Y u%!ZqN=CTJwl$SF$y==G=Vqsw<%y71E{nWjZooPU4F?hQAxvX#3mg?#{p;WTu7CVB&4MkH?P4^aKV$NqHU(*hAfC%SixVG3@HS?;h4UCQ zv~?OWEa8~RTg)4m-@uY6mdKI;Vst0P%Z1asU7T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1e8e586dd57d040c3b458993cfacb696388dd013 GIT binary patch literal 273 zcmV+s0q*{ZP)}mVi!uBUwBY%l>m&8QIDqeegcZYo9Z`n=@*)hxN2dbN5EW4fRu&~e bD%*v_@$kQ>#00000NkvXXu0mjflyZ2z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..da080aa4f0c2625df0da67196b52465c9390aad0 GIT binary patch literal 270 zcmV+p0rCEcP)b6@1A+v$8S3TR0ncnbA=HBQG^-WZE5z>R zV5n$K>@CiNPDCaGaNLUS_bh7&MWgnrGp^Q@%p#;@yh)Mez4@0Yq|p UW2Kn;g^! zDW=V`OBj}LMmk^Ps^4M3@z|6hP1(orgo1!u8K)n!LzlxgMQtGSOm@X##+z;d42O6e wmUS92+)SI%{oz*MJiZIelOM5|R($4SP*D2I``r6wG|*`bp00i_>zopr02WO}Pyhe` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..83cea2b2f5ca05a1a681a86e2b314cc5e3153dd3 GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`^F3W0Ln>}1KX{msynw}+r;d5c z6PN$Z1{c{5m>k%!ypv1fu@Q^2vB$>EjY@OO!l$!s&fW6lUve>TSDJ9BW=HQatpc+X z0s_)03>|EVDb5Bt?3xUjPS+Avsj-T)C+=Z8kkg{cCdqQ>gxgxt2B9Cyq1ykxL7C{)#G(wy*QipTdeEDSepEqeN<{^xq2 PQyDy6{an^LB{Ts5++G=pf6oQ)dOe+3b22IvU> zPxhAiA85}0-&TwDzoi;82KF-M{qJwi{a+QE3qZg~ndN_WfcXD>e~JG&fiNrv#S;HR z%-Q~{3p3ykOpx=G#TfnvTJZeO^^y8t9KiQK!iwR)jwr)_c@YNUqf-HBh>9o#D~pn# bap(a69uK7-kTu6I00000NkvXXu0mjfHR5;E literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ed1f95533c7c239afd8981a4aa9a5509e4fe875c GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`4W2HJAr-fh6D0O6Sg!WSo@esX zk2mMq{@0g{W;x~}%@*|G>D{gWcVFAmm^p({;cx=mBNG-C#^9cHk}k^KiKZ-R);`P~ zY&(H;hwlZ38E;SgYtLl4XwY+=VMbcX?TNgHr1=bbPi#}#{P7se8POXiLR(~`nVKX$ i$`&}1|M>sko>_H9;uVG# z#uEqPX8*P~>siCLn$e(DT+jD!{m)l_{`1S{vs^Z?+vF(0)}WFgbzqkK+xE)}g%WI? zC%cXQ9?xo6EvPS{yqn>ss6pi%Hj!VS|MPEq(xUBhT)|Si)a(sgLqnsI`VD~`iAdvf z4lIllm;~4|9j@^RAD8An!LW=mR@slaEiZ_TVPZf@L#563XF%66c)I$ztaD0e0sxYx BN16Zt literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8d3c91787d3a15ca5504ccc3482df6e7c5e7aff9 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`lRRA}1|M>sko>_H9;uVG# z#uEqPX8*P~>siCLn$e)u+Hc#x`?bqn|L=^xe88lqKS!XAQGs&?n}A(qHs2h!&cg;C zK5?Wkn?L&#>R#0EnUyHTd#U8XtOTj;Z_oa` zADZ2LpJ9UAjI%GK9BcNp9f)DL#B-@*9ivP0k&jXhZ%QUOmH4vkZOF5BtWP#(NE3Ec re{9Ua)^Ux8>BTHhHW6uuat4NS&((4Tc~2$)-NfMO>gTe~DWM4fY)46? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a73628790fbcdb373e4482d61a8da4fdf38ec744 GIT binary patch literal 336 zcmV-W0k8gvP)dz;)_@eK8skcB3NV(uwW2Y7IZ;_%%T>9B1^~pXFFrc zHU$@c?so3^zx$tErie)WBb7VAb&r&y?|^o{PMuzxJkO)+<;=pfX`19X4rKyhYz?Xe zh=3a18k@D&G@p*3WB?+{i7-Nd08nX`C~GWbS=L)zc@}fpSP;fihGD#I+P2Lb#d3j? z#)8m?p2Kbx03akm@qXLVX1$=au>^=d2I$FD;^$S3S iG2`Mo06r+6zwrenR`^_evE$MJ0000!7M|NfjV9{`y#>G+ zO9u%M0Yzg0Hkb80mFii@8Gwj#B8(6q0Qkc`dN-Ep&8tsw7ByU=3rUp z4X@8bPGdpnL(gJ_cyD4DBvmE;yO(>BNi`9!X$9W*d!1 zcs0sc_7oPtFthMdY38_D46-5_43K;e^c#9yqd??tv1uPm%n(jT5|}N(tISOcA}ea< n&8Tkx%8Y3j*8wm<|M?s5!9cM!s`L=A00000NkvXXu0mjf4UmsS literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 43e1e69..4a04a3a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Cooking makes food an interactive part of everyday life. Players grow and gather - **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`). - **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`). +- **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 5d5cb03..ea7f671 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java @@ -297,12 +297,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)); } } @@ -366,6 +374,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); } @@ -923,10 +933,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 fd1e3ab..d0ef819 100644 --- a/src/main/resources/conversions.yml +++ b/src/main/resources/conversions.yml @@ -45,7 +45,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 c18f3f1..9ccbadd 100644 --- a/src/main/resources/models.yml +++ b/src/main/resources/models.yml @@ -3604,6 +3604,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 cd71708..144b8b9 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 # Tags a soup earns from what's in the pot when it's ladled, written like sandwiches.yml's bonuses. The pot has no # layers, so only count: origin applies, over everything in it (pieces, extras and stock). diff --git a/src/main/resources/types.yml b/src/main/resources/types.yml index 803ad37..30fcdb8 100644 --- a/src/main/resources/types.yml +++ b/src/main/resources/types.yml @@ -524,6 +524,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"); + } +}