From 13a7f08665e408c8d26016eb4e4e6b43823bda1d Mon Sep 17 00:00:00 2001
From: GanyusLeftHorn <1244229+GanyusLeftHorn@users.noreply.github.com>
Date: Sat, 2 Jul 2022 02:14:43 -0700
Subject: [PATCH] Add character's specialty food.

---
 .../java/emu/grasscutter/data/GameData.java   |  5 ++
 .../data/excels/CookBonusData.java            | 45 ++++++++++
 .../game/managers/CookingManager.java         | 83 ++++++++++++++-----
 .../packet/send/PacketPlayerCookRsp.java      | 20 +++--
 4 files changed, 122 insertions(+), 31 deletions(-)
 create mode 100644 src/main/java/emu/grasscutter/data/excels/CookBonusData.java

diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java
index 56f420d6..89bf06df 100644
--- a/src/main/java/emu/grasscutter/data/GameData.java
+++ b/src/main/java/emu/grasscutter/data/GameData.java
@@ -99,6 +99,7 @@ public class GameData {
 	private static final Int2ObjectMap<BattlePassMissionData> battlePassMissionDataMap = new Int2ObjectOpenHashMap<>();
 	private static final Int2ObjectMap<BattlePassRewardData> battlePassRewardDataMap = new Int2ObjectOpenHashMap<>();
 	private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap = new Int2ObjectOpenHashMap<>();
+	private static final Int2ObjectMap<CookBonusData> cookBonusDataMap = new Int2ObjectOpenHashMap<>();
 
 	@Getter private static final Int2ObjectMap<ActivityData> activityDataMap = new Int2ObjectOpenHashMap<>();
     @Getter private static final Int2ObjectMap<ActivityWatcherData> activityWatcherDataMap = new Int2ObjectOpenHashMap<>();
@@ -441,4 +442,8 @@ public class GameData {
 	public static Int2ObjectMap<CookRecipeData> getCookRecipeDataMap() {
 		return cookRecipeDataMap;
 	}
+
+	public static Int2ObjectMap<CookBonusData> getCookBonusDataMap() {
+		return cookBonusDataMap;
+	}
 }
diff --git a/src/main/java/emu/grasscutter/data/excels/CookBonusData.java b/src/main/java/emu/grasscutter/data/excels/CookBonusData.java
new file mode 100644
index 00000000..9e9ef4e7
--- /dev/null
+++ b/src/main/java/emu/grasscutter/data/excels/CookBonusData.java
@@ -0,0 +1,45 @@
+package emu.grasscutter.data.excels;
+
+import java.util.List;
+
+import emu.grasscutter.data.GameResource;
+import emu.grasscutter.data.ResourceType;
+import emu.grasscutter.data.ResourceType.LoadPriority;
+import emu.grasscutter.data.common.ItemParamData;
+
+@ResourceType(name = {"CookBonusExcelConfigData.json"}, loadPriority = LoadPriority.LOW)
+public class CookBonusData extends GameResource {
+    private int avatarId;
+    private int recipeId;
+    private int[] paramVec;
+    private int[] complexParamVec;
+
+    @Override
+	public int getId() {
+		return this.avatarId;
+	}
+
+    public int getAvatarId() {
+        return avatarId;
+    }
+
+    public int getRecipeId() {
+        return recipeId;
+    }
+
+    public int[] getParamVec() {
+        return paramVec;
+    }
+
+    public int[] getComplexParamVec() {
+        return complexParamVec;
+    }
+
+    public int getReplacementItemId() {
+        return this.paramVec[0];
+    }
+
+    @Override
+    public void onLoad() {
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/managers/CookingManager.java b/src/main/java/emu/grasscutter/game/managers/CookingManager.java
index bcc31bb3..855fd56e 100644
--- a/src/main/java/emu/grasscutter/game/managers/CookingManager.java
+++ b/src/main/java/emu/grasscutter/game/managers/CookingManager.java
@@ -20,6 +20,7 @@ import emu.grasscutter.server.packet.send.PacketCookDataNotify;
 import emu.grasscutter.server.packet.send.PacketCookRecipeDataNotify;
 import emu.grasscutter.server.packet.send.PacketPlayerCookArgsRsp;
 import emu.grasscutter.server.packet.send.PacketPlayerCookRsp;
+import io.netty.util.internal.ThreadLocalRandom;
 
 public class CookingManager {
     private static final int MANUAL_PERFECT_COOK_QUALITY = 3;
@@ -46,33 +47,44 @@ public class CookingManager {
      * Unlocking for recipies.
      ********************/
     public synchronized boolean unlockRecipe(GameItem recipeItem) {
-		// Make sure this is actually a cooking recipe.
-		if (!recipeItem.getItemData().getItemUse().get(0).getUseOp().equals("ITEM_USE_UNLOCK_COOK_RECIPE")) {
-			return false;
-		}
+        // Make sure this is actually a cooking recipe.
+        if (!recipeItem.getItemData().getItemUse().get(0).getUseOp().equals("ITEM_USE_UNLOCK_COOK_RECIPE")) {
+            return false;
+        }
 
-		// Determine the recipe we should unlock.
-		int recipeId = Integer.parseInt(recipeItem.getItemData().getItemUse().get(0).getUseParam().get(0));
+        // Determine the recipe we should unlock.
+        int recipeId = Integer.parseInt(recipeItem.getItemData().getItemUse().get(0).getUseParam().get(0));
 
-		// Remove the item from the player's inventory.
-		// We need to do this here, before sending CookRecipeDataNotify, or the the UI won't correctly update.
-		player.getInventory().removeItem(recipeItem, 1);
+        // Remove the item from the player's inventory.
+        // We need to do this here, before sending CookRecipeDataNotify, or the the UI won't correctly update.
+        player.getInventory().removeItem(recipeItem, 1);
 
-		// Tell the client that this blueprint is now unlocked and add the unlocked item to the player.
-		this.player.getUnlockedRecipies().put(recipeId, 0);
-		this.player.sendPacket(new PacketCookRecipeDataNotify(recipeId));
+        // Tell the client that this blueprint is now unlocked and add the unlocked item to the player.
+        this.player.getUnlockedRecipies().put(recipeId, 0);
+        this.player.sendPacket(new PacketCookRecipeDataNotify(recipeId));
 
-		return true;
-	}
+        return true;
+    }
 
     /********************
      * Perform cooking.
      ********************/
+    private double getSpecialtyChance(ItemData cookedItem) {
+        // Chances taken from the Wiki.
+        return switch (cookedItem.getRankLevel()) {
+            case 1 -> 0.25;
+            case 2 -> 0.2;
+            case 3 -> 0.15;
+            default -> 0;
+        };
+    }
+
     public void handlePlayerCookReq(PlayerCookReq req) {
         // Get info from the request.
         int recipeId = req.getRecipeId();
         int quality = req.getQteQuality();
         int count = req.getCookCount();
+        int avatar = req.getAssistAvatar();
 
         // Get recipe data.
         var recipeData = GameData.getCookRecipeDataMap().get(recipeId);
@@ -85,12 +97,12 @@ public class CookingManager {
         int proficiency = this.player.getUnlockedRecipies().getOrDefault(recipeId, 0);
 
         // Try consuming materials.
-		boolean success = player.getInventory().payItems(recipeData.getInputVec().toArray(new ItemParamData[0]), count, ActionReason.Cook);
-		if (!success) {
-			this.player.sendPacket(new PacketPlayerCookRsp(Retcode.RET_FAIL)); //ToDo: Probably the wrong return code.
-		}
+        boolean success = player.getInventory().payItems(recipeData.getInputVec().toArray(new ItemParamData[0]), count, ActionReason.Cook);
+        if (!success) {
+            this.player.sendPacket(new PacketPlayerCookRsp(Retcode.RET_FAIL));
+        }
 
-        // Obtain results.
+        // Get result item information.
         int qualityIndex = 
             quality == 0 
             ? 2 
@@ -99,8 +111,34 @@ public class CookingManager {
         ItemParamData resultParam = recipeData.getQualityOutputVec().get(qualityIndex);
         ItemData resultItemData = GameData.getItemDataMap().get(resultParam.getItemId());
 
-		GameItem cookResult = new GameItem(resultItemData, resultParam.getCount() * count);
-		this.player.getInventory().addItem(cookResult);
+        // Handle character's specialties.
+        int specialtyCount = 0;
+        double specialtyChance = this.getSpecialtyChance(resultItemData);
+
+        var bonusData = GameData.getCookBonusDataMap().get(avatar);
+        if (bonusData != null && recipeId == bonusData.getRecipeId()) {
+            // Roll for specialy replacements.
+            for (int i = 0; i < count; i++) {
+                if (ThreadLocalRandom.current().nextDouble() <= specialtyChance) {
+                    specialtyCount++;
+                }
+            }
+        }
+
+        // Obtain results.
+        List<GameItem> cookResults = new ArrayList<>();
+
+        int normalCount = count - specialtyCount;
+        GameItem cookResultNormal = new GameItem(resultItemData, resultParam.getCount() * normalCount);
+        cookResults.add(cookResultNormal);
+        this.player.getInventory().addItem(cookResultNormal);
+
+        if (specialtyCount > 0) {
+            ItemData specialtyItemData = GameData.getItemDataMap().get(bonusData.getReplacementItemId());
+            GameItem cookResultSpecialty = new GameItem(specialtyItemData, resultParam.getCount() * specialtyCount);
+            cookResults.add(cookResultSpecialty);
+            this.player.getInventory().addItem(cookResultSpecialty);
+        }
 
         // Increase player proficiency, if this was a manual perfect cook.
         if (quality == MANUAL_PERFECT_COOK_QUALITY) {
@@ -109,10 +147,9 @@ public class CookingManager {
         }
 
         // Send response.
-        this.player.sendPacket(new PacketPlayerCookRsp(cookResult, quality, count, recipeId, proficiency));
+        this.player.sendPacket(new PacketPlayerCookRsp(cookResults, quality, count, recipeId, proficiency));
     }
 
-
     /********************
      * Cooking arguments.
      ********************/
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerCookRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerCookRsp.java
index 517ba36e..8a3d6d5d 100644
--- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerCookRsp.java
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerCookRsp.java
@@ -1,5 +1,7 @@
 package emu.grasscutter.server.packet.send;
 
+import java.util.List;
+
 import emu.grasscutter.game.inventory.GameItem;
 import emu.grasscutter.net.packet.BasePacket;
 import emu.grasscutter.net.packet.PacketOpcodes;
@@ -19,23 +21,25 @@ public class PacketPlayerCookRsp extends BasePacket {
         this.setData(proto);
     }
 
-    public PacketPlayerCookRsp(GameItem output, int quality, int count, int recipeId, int proficiency) {
+    public PacketPlayerCookRsp(List<GameItem> output, int quality, int count, int recipeId, int proficiency) {
         super(PacketOpcodes.PlayerCookRsp);
 
-        PlayerCookRsp proto = PlayerCookRsp.newBuilder()
+        PlayerCookRsp.Builder proto = PlayerCookRsp.newBuilder()
             .setRecipeData(
                 CookRecipeData.newBuilder()
                     .setRecipeId(recipeId)
                     .setProficiency(proficiency)    
             )
             .setQteQuality(quality)
-            .addItemList(
+            .setCookCount(count);
+
+        for (var item : output) {
+            proto.addItemList(
                 ItemParam.newBuilder()
-                    .setItemId(output.getItemId())
-                    .setCount(output.getCount())
-            )
-            .setCookCount(count)
-            .build();
+                    .setItemId(item.getItemId())
+                    .setCount(item.getCount())
+            );
+        }
 
         this.setData(proto);
     }
-- 
GitLab