From fb1bacb0f814c65907c8c187ec109e93a6f52425 Mon Sep 17 00:00:00 2001
From: AnimeGitB <AnimeGitB@bigblueball.in>
Date: Wed, 17 Aug 2022 19:48:41 +0930
Subject: [PATCH] Refactor avatar skilldepot and constellation/talent changing
 Ensures Traveler retains talent levels and constellations on inactive
 elements when switching elements. Relevant for any other skillDepot-changing
 activities like Windtrace too, though keeping those in the db might not be as
 useful.

Refactor avatar talent upgrade and access
Refactor skillExtraCharges
---
 .../command/commands/GiveCommand.java         |  23 +-
 .../command/commands/ResetConstCommand.java   |   5 +-
 .../command/commands/SetConstCommand.java     |  17 +-
 .../command/commands/TalentCommand.java       |  91 +++--
 .../data/excels/AvatarSkillDepotData.java     |   6 +
 .../data/excels/ProudSkillData.java           | 136 +++-----
 .../emu/grasscutter/game/avatar/Avatar.java   | 328 ++++++++++++------
 .../grasscutter/game/player/TeamManager.java  |   6 +-
 .../game/systems/InventorySystem.java         | 115 +-----
 .../emu/grasscutter/game/world/Scene.java     |  16 +-
 .../HandlerAvatarChangeElementTypeReq.java    |  89 +++--
 .../recv/HandlerAvatarSkillUpgradeReq.java    |   6 +-
 .../recv/HandlerUnlockAvatarTalentReq.java    |   4 +-
 .../send/PacketAvatarSkillInfoNotify.java     |  25 +-
 14 files changed, 395 insertions(+), 472 deletions(-)

diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java
index a32a6cc8..b6f29e84 100644
--- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java
+++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java
@@ -244,32 +244,11 @@ public final class GiveCommand implements CommandHandler {
     }
 
     private static Avatar makeAvatar(AvatarData avatarData, int level, int promoteLevel, int constellation) {
-        // Calculate ascension level.
         Avatar avatar = new Avatar(avatarData);
         avatar.setLevel(level);
         avatar.setPromoteLevel(promoteLevel);
-
-        // Add constellations.
-        int talentBase = switch (avatar.getAvatarId()) {
-            case 10000005 -> 70;
-            case 10000006 -> 40;
-            default -> (avatar.getAvatarId() - 10000000) * 10;
-        };
-
-        for (int i = 1; i <= constellation; i++) {
-            avatar.getTalentIdList().add(talentBase + i);
-        }
-
-        // Main character needs skill depot manually added.
-        if (avatar.getAvatarId() == GameConstants.MAIN_CHARACTER_MALE) {
-            avatar.setSkillDepotData(GameData.getAvatarSkillDepotDataMap().get(504));
-        }
-        else if (avatar.getAvatarId() == GameConstants.MAIN_CHARACTER_FEMALE) {
-            avatar.setSkillDepotData(GameData.getAvatarSkillDepotDataMap().get(704));
-        }
-
+        avatar.forceConstellationLevel(constellation);
         avatar.recalcStats();
-
         return avatar;
     }
 
diff --git a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java
index 5c4aa984..135ec454 100644
--- a/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java
+++ b/src/main/java/emu/grasscutter/command/commands/ResetConstCommand.java
@@ -37,9 +37,6 @@ public final class ResetConstCommand implements CommandHandler {
     }
 
     private void resetConstellation(Avatar avatar) {
-        avatar.getTalentIdList().clear();
-        avatar.setCoreProudSkillLevel(0);
-        avatar.recalcStats();
-        avatar.save();
+        avatar.forceConstellationLevel(-1);
     }
 }
diff --git a/src/main/java/emu/grasscutter/command/commands/SetConstCommand.java b/src/main/java/emu/grasscutter/command/commands/SetConstCommand.java
index dfdb1169..f24f46fc 100644
--- a/src/main/java/emu/grasscutter/command/commands/SetConstCommand.java
+++ b/src/main/java/emu/grasscutter/command/commands/SetConstCommand.java
@@ -15,7 +15,6 @@ import emu.grasscutter.utils.Position;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 
 import java.util.List;
-import java.util.Set;
 
 @Command(
     label = "setConst",
@@ -33,7 +32,7 @@ public final class SetConstCommand implements CommandHandler {
 
         try {
             int constLevel = Integer.parseInt(args.get(0));
-            if (constLevel < 0 || constLevel > 6) {
+            if (constLevel < -1 || constLevel > 6) {
                 CommandHandler.sendTranslatedMessage(sender, "commands.setConst.range_error");
                 return;
             }
@@ -52,19 +51,7 @@ public final class SetConstCommand implements CommandHandler {
 
     private void setConstellation(Player player, Avatar avatar, int constLevel) {
         int currentConstLevel = avatar.getCoreProudSkillLevel();
-        IntArrayList talentIds = new IntArrayList(avatar.getSkillDepot().getTalents());
-        Set<Integer> talentIdList = avatar.getTalentIdList();
-
-        talentIdList.clear();
-        avatar.setCoreProudSkillLevel(0);
-
-        for(int talent = 0; talent < constLevel; talent++) {
-            AvatarTalentData talentData = GameData.getAvatarTalentDataMap().get(talentIds.getInt(talent));
-            int mainCostItemId = talentData.getMainCostItemId();
-
-            player.getInventory().addItem(mainCostItemId);
-            Grasscutter.getGameServer().getInventorySystem().unlockAvatarConstellation(player, avatar.getGuid());
-        }
+        avatar.forceConstellationLevel(constLevel);
 
         // force player to reload scene when necessary
         if (constLevel < currentConstLevel) {
diff --git a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java
index 226203d8..c1ac1d38 100644
--- a/src/main/java/emu/grasscutter/command/commands/TalentCommand.java
+++ b/src/main/java/emu/grasscutter/command/commands/TalentCommand.java
@@ -4,10 +4,7 @@ import emu.grasscutter.command.Command;
 import emu.grasscutter.command.CommandHandler;
 import emu.grasscutter.data.excels.AvatarSkillDepotData;
 import emu.grasscutter.game.avatar.Avatar;
-import emu.grasscutter.game.entity.EntityAvatar;
 import emu.grasscutter.game.player.Player;
-import emu.grasscutter.server.packet.send.PacketAvatarSkillChangeNotify;
-import emu.grasscutter.server.packet.send.PacketAvatarSkillUpgradeRsp;
 
 import java.util.List;
 
@@ -17,34 +14,6 @@ import java.util.List;
     permission = "player.settalent",
     permissionTargeted = "player.settalent.others")
 public final class TalentCommand implements CommandHandler {
-    private void setTalentLevel(Player sender, Player player, Avatar avatar, int talentId, int talentLevel) {
-        var skillLevelMap = avatar.getSkillLevelMap();
-        int oldLevel = skillLevelMap.get(talentId);
-        if (talentLevel < 0 || talentLevel > 15) {
-            CommandHandler.sendTranslatedMessage(sender, "commands.talent.lower_16");
-            return;
-        }
-
-        // Upgrade skill
-        skillLevelMap.put(talentId, talentLevel);
-        avatar.save();
-
-        // Packet
-        player.sendPacket(new PacketAvatarSkillChangeNotify(avatar, talentId, oldLevel, talentLevel));
-        player.sendPacket(new PacketAvatarSkillUpgradeRsp(avatar, talentId, oldLevel, talentLevel));
-
-        String successMessage = "commands.talent.set_id";
-        AvatarSkillDepotData depot = avatar.getSkillDepot();
-        if (talentId == depot.getSkills().get(0)) {
-            successMessage = "commands.talent.set_atk";
-        } else if (talentId == depot.getSkills().get(1)) {
-            successMessage = "commands.talent.set_e";
-        } else if (talentId == depot.getEnergySkill()) {
-            successMessage = "commands.talent.set_q";
-        }
-        CommandHandler.sendTranslatedMessage(sender, successMessage, talentLevel);
-    }
-
     @Override
     public void execute(Player sender, Player targetPlayer, List<String> args) {
         if (args.size() < 1){
@@ -52,9 +21,19 @@ public final class TalentCommand implements CommandHandler {
             return;
         }
 
-        EntityAvatar entity = targetPlayer.getTeamManager().getCurrentAvatarEntity();
-        Avatar avatar = entity.getAvatar(); 
-        String cmdSwitch = args.get(0);
+        Avatar avatar = targetPlayer.getTeamManager().getCurrentAvatarEntity().getAvatar(); 
+        AvatarSkillDepotData skillDepot = avatar.getSkillDepot();
+        if (skillDepot == null) {  // Avatars without skill depots aren't a suitable target even with manual skillId specified
+            CommandHandler.sendTranslatedMessage(sender, "commands.talent.invalid_skill_id");
+            return;
+        }
+        int skillIdNorAtk = skillDepot.getSkills().get(0);
+        int skillIdE = skillDepot.getSkills().get(1);
+        int skillIdQ = skillDepot.getEnergySkill();
+        int skillId = 0;
+        int newLevel = -1;
+
+        String cmdSwitch = args.get(0).toLowerCase();
         switch (cmdSwitch) {
             default -> {
                 sendUsageMessage(sender);
@@ -62,42 +41,56 @@ public final class TalentCommand implements CommandHandler {
             }
             case "set" -> {
                 if (args.size() < 3) {
-                    sendUsageMessage(sender);
                     sendUsageMessage(sender);
                     return;
                 }
                 try {
-                    int skillId = Integer.parseInt(args.get(1));
-                    int newLevel = Integer.parseInt(args.get(2));
-                    setTalentLevel(sender, targetPlayer, avatar, skillId, newLevel);
+                    skillId = Integer.parseInt(args.get(1));
                 } catch (NumberFormatException ignored) {
                     CommandHandler.sendTranslatedMessage(sender, "commands.talent.invalid_skill_id");
                     return;
                 }
+                try {
+                    newLevel = Integer.parseInt(args.get(2));
+                } catch (NumberFormatException ignored) {
+                    CommandHandler.sendTranslatedMessage(sender, "commands.talent.invalid_skill_level");
+                    return;
+                }
+
+                if (avatar.setSkillLevel(skillId, newLevel)) {
+                    CommandHandler.sendTranslatedMessage(sender, "commands.talent.set_id", newLevel);
+                } else {
+                    CommandHandler.sendTranslatedMessage(sender, "commands.talent.lower_16");
+                }
             }
             case "n", "e", "q" -> {
                 if (args.size() < 2) {
                     sendUsageMessage(sender);
                     return;
                 }
-                AvatarSkillDepotData SkillDepot = avatar.getSkillDepot();
-                int skillId = switch (cmdSwitch) {
-                    default -> SkillDepot.getSkills().get(0);
-                    case "e" -> SkillDepot.getSkills().get(1);
-                    case "q" -> SkillDepot.getEnergySkill();
-                };
                 try {
-                    int newLevel = Integer.parseInt(args.get(1));
-                    setTalentLevel(sender, targetPlayer, avatar, skillId, newLevel);
+                    newLevel = Integer.parseInt(args.get(1));
                 } catch (NumberFormatException ignored) {
                     CommandHandler.sendTranslatedMessage(sender, "commands.talent.invalid_level");
                     return;
                 }
+
+                skillId = switch (cmdSwitch) {
+                    default -> skillIdNorAtk;
+                    case "e" -> skillIdE;
+                    case "q" -> skillIdQ;
+                };
+                if (avatar.setSkillLevel(skillId, newLevel)) {
+                    switch (cmdSwitch) {
+                        default -> CommandHandler.sendTranslatedMessage(sender, "commands.talent.set_atk", newLevel);
+                        case "e" -> CommandHandler.sendTranslatedMessage(sender, "commands.talent.set_e", newLevel);
+                        case "q" -> CommandHandler.sendTranslatedMessage(sender, "commands.talent.set_q", newLevel);
+                    }
+                } else {
+                    CommandHandler.sendTranslatedMessage(sender, "commands.talent.lower_16");
+                }
             }
             case "getid" -> {
-                int skillIdNorAtk = avatar.getSkillDepot().getSkills().get(0);
-                int skillIdE = avatar.getSkillDepot().getSkills().get(1);
-                int skillIdQ = avatar.getSkillDepot().getEnergySkill();
                 CommandHandler.sendTranslatedMessage(sender, "commands.talent.normal_attack_id", Integer.toString(skillIdNorAtk));
                 CommandHandler.sendTranslatedMessage(sender, "commands.talent.e_skill_id", Integer.toString(skillIdE));
                 CommandHandler.sendTranslatedMessage(sender, "commands.talent.q_skill_id", Integer.toString(skillIdQ));
diff --git a/src/main/java/emu/grasscutter/data/excels/AvatarSkillDepotData.java b/src/main/java/emu/grasscutter/data/excels/AvatarSkillDepotData.java
index 9d775f5f..2fbbd72f 100644
--- a/src/main/java/emu/grasscutter/data/excels/AvatarSkillDepotData.java
+++ b/src/main/java/emu/grasscutter/data/excels/AvatarSkillDepotData.java
@@ -1,6 +1,7 @@
 package emu.grasscutter.data.excels;
 
 import java.util.List;
+import java.util.stream.IntStream;
 
 import emu.grasscutter.data.GameData;
 import emu.grasscutter.data.GameDepot;
@@ -71,4 +72,9 @@ public class AvatarSkillDepotData extends GameResource {
         @Getter private int proudSkillGroupId;
         @Getter private int needAvatarPromoteLevel;
     }
+
+    public IntStream getSkillsAndEnergySkill() {
+        return IntStream.concat(this.skills.stream().mapToInt(i -> i), IntStream.of(this.energySkill))
+                        .filter(skillId -> skillId > 0);
+    }
 }
diff --git a/src/main/java/emu/grasscutter/data/excels/ProudSkillData.java b/src/main/java/emu/grasscutter/data/excels/ProudSkillData.java
index 34701ffe..e16bf039 100644
--- a/src/main/java/emu/grasscutter/data/excels/ProudSkillData.java
+++ b/src/main/java/emu/grasscutter/data/excels/ProudSkillData.java
@@ -3,99 +3,59 @@ package emu.grasscutter.data.excels;
 import java.util.ArrayList;
 import java.util.List;
 
+import dev.morphia.annotations.Transient;
 import emu.grasscutter.data.GameResource;
 import emu.grasscutter.data.ResourceType;
 import emu.grasscutter.data.common.FightPropData;
 import emu.grasscutter.data.common.ItemParamData;
+import lombok.Getter;
 
 @ResourceType(name = "ProudSkillExcelConfigData.json")
 public class ProudSkillData extends GameResource {
-	
-	private int proudSkillId;
-    private int proudSkillGroupId;
-    private int level;
-    private int coinCost;
-    private int breakLevel;
-    private int proudSkillType;
-    private String openConfig;
-    private List<ItemParamData> costItems;
-    private List<String> filterConds;
-    private List<String> lifeEffectParams;
-    private FightPropData[] addProps;
-    private float[] paramList;
-    private long[] paramDescList;
-    private long nameTextMapHash;
-	
-	@Override
-	public int getId() {
-		return proudSkillId;
-	}
-
-	public int getProudSkillGroupId() {
-		return proudSkillGroupId;
-	}
-
-	public int getLevel() {
-		return level;
-	}
-
-	public int getCoinCost() {
-		return coinCost;
-	}
-
-	public int getBreakLevel() {
-		return breakLevel;
-	}
-
-	public int getProudSkillType() {
-		return proudSkillType;
-	}
-
-	public String getOpenConfig() {
-		return openConfig;
-	}
-
-	public List<ItemParamData> getCostItems() {
-		return costItems;
-	}
-
-	public List<String> getFilterConds() {
-		return filterConds;
-	}
-
-	public List<String> getLifeEffectParams() {
-		return lifeEffectParams;
-	}
-
-	public FightPropData[] getAddProps() {
-		return addProps;
-	}
-
-	public float[] getParamList() {
-		return paramList;
-	}
-
-	public long[] getParamDescList() {
-		return paramDescList;
-	}
-
-	public long getNameTextMapHash() {
-		return nameTextMapHash;
-	}
-
-	@Override
-	public void onLoad() {
-		if (this.getOpenConfig() != null & this.getOpenConfig().length() > 0) {
-			this.openConfig = "Avatar_" + this.getOpenConfig();
-		}
-		// Fight props
-		ArrayList<FightPropData> parsed = new ArrayList<FightPropData>(getAddProps().length);
-		for (FightPropData prop : getAddProps()) {
-			if (prop.getPropType() != null && prop.getValue() != 0f) {
-				prop.onLoad();
-				parsed.add(prop);
-			}
-		}
-		this.addProps = parsed.toArray(new FightPropData[parsed.size()]);
-	}
+    private int proudSkillId;
+    @Getter private int proudSkillGroupId;
+    @Getter private int level;
+    @Getter private int coinCost;
+    @Getter private int breakLevel;
+    @Getter private int proudSkillType;
+    @Getter private String openConfig;
+    @Getter private List<ItemParamData> costItems;
+    @Getter private List<String> filterConds;
+    @Getter private List<String> lifeEffectParams;
+    @Getter private FightPropData[] addProps;
+    @Getter private float[] paramList;
+    @Getter private long[] paramDescList;
+    @Getter private long nameTextMapHash;
+    @Transient private Iterable<ItemParamData> totalCostItems;
+
+    @Override
+    public int getId() {
+        return proudSkillId;
+    }
+
+    public Iterable<ItemParamData> getTotalCostItems() {
+        if (this.totalCostItems == null) {
+            ArrayList<ItemParamData> total = (this.costItems != null) ? new ArrayList<>(this.costItems) : new ArrayList<>(1);
+            if (this.coinCost > 0)
+                total.add(new ItemParamData(202, this.coinCost));
+            this.totalCostItems = total;
+        }
+        return this.totalCostItems;
+    }
+
+    @Override
+    public void onLoad() {
+        if (this.getOpenConfig() != null & this.getOpenConfig().length() > 0) {
+            this.openConfig = "Avatar_" + this.getOpenConfig();
+        }
+        // Fight props
+        ArrayList<FightPropData> parsed = new ArrayList<FightPropData>(getAddProps().length);
+        for (FightPropData prop : getAddProps()) {
+            if (prop.getPropType() != null && prop.getValue() != 0f) {
+                prop.onLoad();
+                parsed.add(prop);
+            }
+        }
+        this.addProps = parsed.toArray(new FightPropData[parsed.size()]);
+    }
 }
diff --git a/src/main/java/emu/grasscutter/game/avatar/Avatar.java b/src/main/java/emu/grasscutter/game/avatar/Avatar.java
index f1d36025..d178c046 100644
--- a/src/main/java/emu/grasscutter/game/avatar/Avatar.java
+++ b/src/main/java/emu/grasscutter/game/avatar/Avatar.java
@@ -7,7 +7,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.stream.Stream;
 import java.util.Set;
 
@@ -19,6 +19,7 @@ import dev.morphia.annotations.Indexed;
 import dev.morphia.annotations.PostLoad;
 import dev.morphia.annotations.PrePersist;
 import dev.morphia.annotations.Transient;
+import emu.grasscutter.GameConstants;
 import emu.grasscutter.data.GameData;
 import emu.grasscutter.data.binout.OpenConfigEntry;
 import emu.grasscutter.data.binout.OpenConfigEntry.SkillPointModifier;
@@ -56,14 +57,14 @@ import emu.grasscutter.net.proto.FetterDataOuterClass.FetterData;
 import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass;
 import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass.ShowAvatarInfo;
 import emu.grasscutter.net.proto.ShowEquipOuterClass.ShowEquip;
-import emu.grasscutter.server.packet.send.PacketAbilityChangeNotify;
-import emu.grasscutter.server.packet.send.PacketAvatarEquipChangeNotify;
-import emu.grasscutter.server.packet.send.PacketAvatarFightPropNotify;
+import emu.grasscutter.server.packet.send.*;
 import emu.grasscutter.utils.ProtoHelper;
 import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap;
 import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -93,12 +94,11 @@ public class Avatar {
 
     private List<Integer> fetters;
 
-    @Getter private Map<Integer, Integer> skillLevelMap; // Talent levels
+    private Map<Integer, Integer> skillLevelMap; // Talent levels
     private Map<Integer, Integer> skillExtraChargeMap; // Charges
     @Getter private Map<Integer, Integer> proudSkillBonusMap; // Talent bonus levels (from const)
     @Getter private int skillDepotId;
-    @Getter @Setter private int coreProudSkillLevel; // Constellation level
-    @Getter private Set<Integer> talentIdList; // Constellation id list
+    private Set<Integer> talentIdList; // Constellation id list
     @Getter private Set<Integer> proudSkillList; // Character passives
 
     @Getter @Setter private int flyCloak;
@@ -147,7 +147,14 @@ public class Avatar {
             .forEach(id -> this.setFightProperty(id, 0f));
 
         // Skill depot
-        this.setSkillDepotData(data.getSkillDepot());
+        this.setSkillDepotData(switch (this.avatarId) {
+            case GameConstants.MAIN_CHARACTER_MALE ->
+                GameData.getAvatarSkillDepotDataMap().get(504);  // Hack to start with anemo skills
+            case GameConstants.MAIN_CHARACTER_FEMALE ->
+                GameData.getAvatarSkillDepotDataMap().get(704);
+            default ->
+                data.getSkillDepot();
+        });
 
         // Set stats
         this.recalcStats();
@@ -219,32 +226,21 @@ public class Avatar {
         // Set id and depot
         this.skillDepotId = skillDepot.getId();
         this.skillDepot = skillDepot;
-        // Clear, then add skills
-        getSkillLevelMap().clear();
-        if (skillDepot.getEnergySkill() > 0) {
-            getSkillLevelMap().put(skillDepot.getEnergySkill(), 1);
-        }
-        for (int skillId : skillDepot.getSkills()) {
-            if (skillId > 0) {
-                getSkillLevelMap().put(skillId, 1);
-            }
-        }
+        // Add any missing skills
+        this.skillDepot.getSkillsAndEnergySkill()
+            .forEach(skillId -> this.skillLevelMap.putIfAbsent(skillId, 1));
         // Add proud skills
-        this.getProudSkillList().clear();
-        for (InherentProudSkillOpens openData : skillDepot.getInherentProudSkillOpens()) {
-            if (openData.getProudSkillGroupId() == 0) {
-                continue;
-            }
-            if (openData.getNeedAvatarPromoteLevel() <= this.getPromoteLevel()) {
-                int proudSkillId = (openData.getProudSkillGroupId() * 100) + 1;
-                if (GameData.getProudSkillDataMap().containsKey(proudSkillId)) {
-                    this.getProudSkillList().add(proudSkillId);
-                }
-            }
-        }
+        this.proudSkillList.clear();
+        skillDepot.getInherentProudSkillOpens().stream()
+            .filter(openData -> openData.getProudSkillGroupId() > 0)
+            .filter(openData -> openData.getNeedAvatarPromoteLevel() <= this.getPromoteLevel())
+            .mapToInt(openData -> (openData.getProudSkillGroupId() * 100) + 1)
+            .filter(proudSkillId -> GameData.getProudSkillDataMap().containsKey(proudSkillId))
+            .forEach(proudSkillId -> this.proudSkillList.add(proudSkillId));
+        this.recalcStats();
     }
 
-    public Map<Integer, Integer> getSkillExtraChargeMap() {
+    private Map<Integer, Integer> getSkillExtraChargeMap() {
         if (skillExtraChargeMap == null) {
             skillExtraChargeMap = new HashMap<>();
         }
@@ -266,16 +262,12 @@ public class Avatar {
     }
 
     public void setCurrentEnergy(float currentEnergy) {
-        if (this.getSkillDepot() != null && this.getSkillDepot().getEnergySkillData() != null) {
-            ElementType element = this.getSkillDepot().getElementType();
-            this.setFightProperty(element.getMaxEnergyProp(), this.getSkillDepot().getEnergySkillData().getCostElemVal());
-
-            if (GAME_OPTIONS.energyUsage) {
-                this.setFightProperty(element.getCurEnergyProp(), currentEnergy);
-            }
-            else {
-                this.setFightProperty(element.getCurEnergyProp(), this.getSkillDepot().getEnergySkillData().getCostElemVal());
-            }
+        var depot = this.skillDepot;
+        if (depot != null && depot.getEnergySkillData() != null) {
+            ElementType element = depot.getElementType();
+            var maxEnergy = depot.getEnergySkillData().getCostElemVal();
+            this.setFightProperty(element.getMaxEnergyProp(), maxEnergy);
+            this.setFightProperty(element.getCurEnergyProp(), GAME_OPTIONS.energyUsage ? currentEnergy : maxEnergy);
         }
     }
 
@@ -307,6 +299,26 @@ public class Avatar {
         return getFightProperties().getOrDefault(prop.getId(), 0f);
     }
 
+    public Map<Integer, Integer> getSkillLevelMap() {  // Returns a copy of the skill levels for the current skillDepot.
+        var map = new Int2IntOpenHashMap();
+        this.skillDepot.getSkillsAndEnergySkill()
+            .forEach(skillId -> map.computeIfAbsent(skillId, this.skillLevelMap::get));
+        return map;
+    }
+
+    public IntSet getTalentIdList() {  // Returns a copy of the unlocked constellations for the current skillDepot.
+        var talents = new IntOpenHashSet(this.getSkillDepot().getTalents());
+        talents.removeIf(id -> !this.talentIdList.contains(id));
+        return talents;
+    }
+
+    public int getCoreProudSkillLevel() {
+        var lockedTalents = new IntOpenHashSet(this.getSkillDepot().getTalents());
+        lockedTalents.removeAll(this.getTalentIdList());
+        // One below the lowest locked talent, or 6 if there are no locked talents.
+        return lockedTalents.intStream().map(i -> i % 10).min().orElse(7) - 1;
+    }
+
     public boolean equipItem(GameItem item, boolean shouldRecalc) {
         // Sanity check equip type
         EquipType itemEquipType = item.getItemData().getEquipType();
@@ -549,17 +561,13 @@ public class Avatar {
         }
 
         // Constellations
-        if (this.getTalentIdList().size() > 0) {
-            for (int talentId : this.getTalentIdList()) {
-                AvatarTalentData avatarTalentData = GameData.getAvatarTalentDataMap().get(talentId);
-                if (avatarTalentData == null) {
-                    return;
-                }
-
-                // Add any skill strings from this constellation
-                this.addToExtraAbilityEmbryos(avatarTalentData.getOpenConfig(), false);
-            }
-        }
+        this.getTalentIdList().intStream()
+            .mapToObj(GameData.getAvatarTalentDataMap()::get)
+            .filter(Objects::nonNull)
+            .map(AvatarTalentData::getOpenConfig)
+            .filter(Objects::nonNull)
+            .forEach(openConfig -> this.addToExtraAbilityEmbryos(openConfig, false));
+            // Add any skill strings from this constellation
 
         // Set % stats
         this.setFightProperty(
@@ -614,71 +622,179 @@ public class Avatar {
         }
     }
 
+    public void calcConstellation(OpenConfigEntry entry, boolean notifyClient) {
+        if (entry == null) return;
+
+        // Check if new constellation adds +3 to a skill level
+        if (this.calcConstellationExtraLevels(entry) && notifyClient) {
+            // Packet
+            this.getPlayer().sendPacket(new PacketProudSkillExtraLevelNotify(this, entry.getExtraTalentIndex()));
+        }
+        // Check if new constellation adds skill charges
+        if (this.calcConstellationExtraCharges(entry) && notifyClient) {
+            // Packet
+            Stream.of(entry.getSkillPointModifiers())
+                .mapToInt(SkillPointModifier::getSkillId)
+                .forEach(skillId -> {
+                    this.getPlayer().sendPacket(
+                        new PacketAvatarSkillMaxChargeCountNotify(this, skillId, this.getSkillExtraChargeMap().getOrDefault(skillId, 0))
+                    );
+                });
+        }
+    }
+
     public void recalcConstellations() {
         // Clear first
         this.getProudSkillBonusMap().clear();
         this.getSkillExtraChargeMap().clear();
 
         // Sanity checks
-        if (getData() == null || this.skillDepot == null) {
+        if (this.data == null || this.skillDepot == null) {
             return;
         }
 
-        if (this.getTalentIdList().size() > 0) {
-            for (int talentId : this.getTalentIdList()) {
-                AvatarTalentData avatarTalentData = GameData.getAvatarTalentDataMap().get(talentId);
+        this.getTalentIdList().intStream()
+            .mapToObj(GameData.getAvatarTalentDataMap()::get)
+            .filter(Objects::nonNull)
+            .map(AvatarTalentData::getOpenConfig)
+            .filter(Objects::nonNull)
+            .filter(openConfig -> openConfig.length() > 0)
+            .map(GameData.getOpenConfigEntries()::get)
+            .filter(Objects::nonNull)
+            .forEach(e -> this.calcConstellation(e, false));
+    }
 
-                if (avatarTalentData == null || avatarTalentData.getOpenConfig() == null || avatarTalentData.getOpenConfig().length() == 0) {
-                    continue;
-                }
+    private boolean calcConstellationExtraCharges(OpenConfigEntry entry) {
+        var skillPointModifiers = entry.getSkillPointModifiers();
+        if (skillPointModifiers == null) return false;
 
-                // Get open config to find which skill should be boosted
-                OpenConfigEntry entry = GameData.getOpenConfigEntries().get(avatarTalentData.getOpenConfig());
-                if (entry == null) {
-                    continue;
-                }
+        for (var mod : skillPointModifiers) {
+            AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(mod.getSkillId());
 
-                // Check if we can add charges to a skill
-                if (entry.getSkillPointModifiers() != null) {
-                    for (SkillPointModifier mod : entry.getSkillPointModifiers()) {
-                        AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(mod.getSkillId());
+            if (skillData == null) continue;
 
-                        if (skillData == null) continue;
+            int charges = skillData.getMaxChargeNum() + mod.getDelta();
 
-                        int charges = skillData.getMaxChargeNum() + mod.getDelta();
+            this.getSkillExtraChargeMap().put(mod.getSkillId(), charges);
+        }
+        return true;
+    }
 
-                        this.getSkillExtraChargeMap().put(mod.getSkillId(), charges);
-                    }
-                    continue;
-                }
+    private boolean calcConstellationExtraLevels(OpenConfigEntry entry) {
+        int skillId = switch(entry.getExtraTalentIndex()) {
+            case 9 -> this.skillDepot.getEnergySkill();  // Ult skill
+            case 2 -> (this.skillDepot.getSkills().size() >= 2) ? this.skillDepot.getSkills().get(1) : 0;  // E skill
+            default -> 0;
+        };
+        // Sanity check
+        if (skillId == 0) {
+            return false;
+        }
 
-                // Check if a skill can be boosted by +3 levels
-                int skillId = 0;
+        // Get proud skill group id
+        AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId);
 
-                if (entry.getExtraTalentIndex() == 2 && this.skillDepot.getSkills().size() >= 2) {
-                    // E skill
-                    skillId = this.skillDepot.getSkills().get(1);
-                } else if (entry.getExtraTalentIndex() == 9) {
-                    // Ult skill
-                    skillId = this.skillDepot.getEnergySkill();
-                }
+        if (skillData == null) {
+            return false;
+        }
 
-                // Sanity check
-                if (skillId == 0) {
-                    continue;
-                }
+        // Add to bonus list
+        this.getProudSkillBonusMap().put(skillData.getProudSkillGroupId(), 3);
+        return true;
+    }
 
-                // Get proud skill group id
-                AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId);
+    public boolean upgradeSkill(int skillId) {
+        AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId);
+        if (skillData == null) return false;
 
-                if (skillData == null) {
-                    continue;
-                }
+        // Get data for next skill level
+        int newLevel = this.skillLevelMap.getOrDefault(skillId, 0) + 1;
+        if (newLevel > 10) return false;
 
-                // Add to bonus list
-                this.getProudSkillBonusMap().put(skillData.getProudSkillGroupId(), 3);
-            }
+        // Proud skill data
+        int proudSkillId = (skillData.getProudSkillGroupId() * 100) + newLevel;
+        ProudSkillData proudSkill = GameData.getProudSkillDataMap().get(proudSkillId);
+        if (proudSkill == null) return false;
+
+        // Make sure break level is correct
+        if (this.getPromoteLevel() < proudSkill.getBreakLevel()) return false;
+
+        // Pay materials and mora if possible
+        if (!this.getPlayer().getInventory().payItems(proudSkill.getTotalCostItems())) return false;
+
+        // Upgrade skill
+        this.setSkillLevel(skillId, newLevel);
+        return true;
+    }
+
+    public boolean setSkillLevel(int skillId, int level) {
+        if (level < 0 || level > 15) return false;
+        int oldLevel = this.skillLevelMap.getOrDefault(skillId, 0);  // just taking the return value of put would have null concerns
+        this.skillLevelMap.put(skillId, level);
+        this.save();
+
+        // Packet
+        this.getPlayer().sendPacket(new PacketAvatarSkillChangeNotify(this, skillId, oldLevel, level));
+        this.getPlayer().sendPacket(new PacketAvatarSkillUpgradeRsp(this, skillId, oldLevel, level));
+        return true;
+    }
+
+    public boolean unlockConstellation() {
+        return this.unlockConstellation(false);
+    }
+    public boolean unlockConstellation(boolean skipPayment) {
+        int currentTalentLevel = this.getCoreProudSkillLevel();
+        int talentId = this.skillDepot.getTalents().get(currentTalentLevel);
+        return this.unlockConstellation(talentId, skipPayment);
+    }
+    public boolean unlockConstellation(int talentId) {
+        return unlockConstellation(talentId, false);
+    }
+    public boolean unlockConstellation(int talentId, boolean skipPayment) {
+        // Get talent
+        AvatarTalentData talentData = GameData.getAvatarTalentDataMap().get(talentId);
+        if (talentData == null) return false;
+
+        // Pay constellation item if possible
+        if (!skipPayment && !this.getPlayer().getInventory().payItem(talentData.getMainCostItemId(), 1)) {
+            return false;
         }
+
+        // Apply + recalc
+        this.talentIdList.add(talentData.getId());
+
+        // Packet
+        this.getPlayer().sendPacket(new PacketAvatarUnlockTalentNotify(this, talentId));
+        this.getPlayer().sendPacket(new PacketUnlockAvatarTalentRsp(this, talentId));
+
+        // Proud skill bonus map (Extra skills)
+        this.calcConstellation(GameData.getOpenConfigEntries().get(talentData.getOpenConfig()), true);
+
+        // Recalc + save avatar
+        this.recalcStats(true);
+        this.save();
+        return true;
+    }
+
+    public void forceConstellationLevel(int level) {
+        if (level > 6) return;  // Sanity check
+
+        if (level < 0) {  // Special case for resetConst to remove inactive depots too
+            this.talentIdList.clear();
+            this.recalcStats();
+            return;
+        }
+        this.talentIdList.removeAll(this.getTalentIdList());  // Only remove constellations from active depot
+        for (int i = 0; i < level; i++)
+            this.unlockConstellation(true);
+        this.recalcStats();
+    }
+
+    public boolean sendSkillExtraChargeMap() {
+        var map = this.getSkillExtraChargeMap();
+        if (map.isEmpty()) return false;
+        this.getPlayer().sendPacket(new PacketAvatarSkillInfoNotify(this.guid, new Int2IntOpenHashMap(map)));
+        return true;
     }
 
     public EntityAvatar getAsEntity() {
@@ -709,14 +825,11 @@ public class Avatar {
         }
 
 
-        if (this.getFetterList() != null) {
-            for (int i = 0; i < this.getFetterList().size(); i++) {
-                avatarFetter.addFetterList(
-                    FetterData.newBuilder()
-                        .setFetterId(this.getFetterList().get(i))
-                        .setFetterState(FetterState.FINISH.getValue())
-                );
-            }
+        if (this.fetters != null) {
+            this.fetters.forEach(fetterId -> avatarFetter.addFetterList(
+                FetterData.newBuilder()
+                    .setFetterId(fetterId)
+                    .setFetterState(FetterState.FINISH.getValue())));
         }
 
         int cardId = this.getNameCardId();
@@ -742,13 +855,10 @@ public class Avatar {
                 .setWearingFlycloakId(this.getFlyCloak())
                 .setCostumeId(this.getCostume());
 
-        for (Entry<Integer, Integer> entry : this.getSkillExtraChargeMap().entrySet()) {
-            avatarInfo.putSkillMap(entry.getKey(), AvatarSkillInfo.newBuilder().setMaxChargeCount(entry.getValue()).build());
-        }
+        this.getSkillExtraChargeMap().forEach((skillId, count) ->
+            avatarInfo.putSkillMap(skillId, AvatarSkillInfo.newBuilder().setMaxChargeCount(count).build()));
 
-        for (GameItem item : this.getEquips().values()) {
-            avatarInfo.addEquipGuidList(item.getGuid());
-        }
+        this.getEquips().forEach((k, item) -> avatarInfo.addEquipGuidList(item.getGuid()));
 
         avatarInfo.putPropMap(PlayerProperty.PROP_LEVEL.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel()));
         avatarInfo.putPropMap(PlayerProperty.PROP_EXP.getId(), ProtoHelper.newPropValue(PlayerProperty.PROP_EXP, this.getExp()));
diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java
index c6f118ad..6340cf09 100644
--- a/src/main/java/emu/grasscutter/game/player/TeamManager.java
+++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java
@@ -338,11 +338,7 @@ public class TeamManager extends BasePlayerDataManager {
         this.getPlayer().getWorld().broadcastPacket(new PacketSceneTeamUpdateNotify(this.getPlayer()));
 
         // Skill charges packet - Yes, this is official server behavior as of 2.6.0
-        for (EntityAvatar entity : this.getActiveTeam()) {
-            if (entity.getAvatar().getSkillExtraChargeMap().size() > 0) {
-                this.getPlayer().sendPacket(new PacketAvatarSkillInfoNotify(entity.getAvatar()));
-            }
-        }
+        this.getActiveTeam().stream().map(EntityAvatar::getAvatar).forEach(Avatar::sendSkillExtraChargeMap);
 
         // Run callback
         if (responsePacket != null) {
diff --git a/src/main/java/emu/grasscutter/game/systems/InventorySystem.java b/src/main/java/emu/grasscutter/game/systems/InventorySystem.java
index b5e377a4..7cf39362 100644
--- a/src/main/java/emu/grasscutter/game/systems/InventorySystem.java
+++ b/src/main/java/emu/grasscutter/game/systems/InventorySystem.java
@@ -7,14 +7,11 @@ import java.util.Map;
 import java.util.stream.Collectors;
 
 import emu.grasscutter.data.GameData;
-import emu.grasscutter.data.binout.OpenConfigEntry;
-import emu.grasscutter.data.binout.OpenConfigEntry.SkillPointModifier;
 import emu.grasscutter.data.common.ItemParamData;
 import emu.grasscutter.data.common.ItemUseData;
 import emu.grasscutter.data.excels.AvatarPromoteData;
 import emu.grasscutter.data.excels.AvatarSkillData;
 import emu.grasscutter.data.excels.AvatarSkillDepotData;
-import emu.grasscutter.data.excels.AvatarTalentData;
 import emu.grasscutter.data.excels.ItemData;
 import emu.grasscutter.data.excels.ProudSkillData;
 import emu.grasscutter.data.excels.WeaponPromoteData;
@@ -649,120 +646,22 @@ public class InventorySystem extends BaseGameSystem {
         player.sendPacket(new PacketAvatarFetterDataNotify(avatar));
     }
 
+    @Deprecated(forRemoval = true)
     public void upgradeAvatarSkill(Player player, long guid, int skillId) {
         // Sanity checks
         Avatar avatar = player.getAvatars().getAvatarByGuid(guid);
-        if (avatar == null) {
-            return;
-        }
-
-        // Make sure avatar has skill
-        if (!avatar.getSkillLevelMap().containsKey(skillId)) {
-            return;
-        }
-
-        AvatarSkillData skillData = GameData.getAvatarSkillDataMap().get(skillId);
-        if (skillData == null) {
-            return;
-        }
-
-        // Get data for next skill level
-        int currentLevel = avatar.getSkillLevelMap().get(skillId);
-        int nextLevel = currentLevel + 1;
-        int proudSkillId = (skillData.getProudSkillGroupId() * 100) + nextLevel;
-
-        // Capped at level 10 talent
-        if (nextLevel > 10) {
-            return;
-        }
+        if (avatar == null) return;
 
-        // Proud skill data
-        ProudSkillData proudSkill = GameData.getProudSkillDataMap().get(proudSkillId);
-        if (proudSkill == null) {
-            return;
-        }
-
-        // Make sure break level is correct
-        if (avatar.getPromoteLevel() < proudSkill.getBreakLevel()) {
-            return;
-        }
-
-        // Pay materials and mora if possible
-        List<ItemParamData> costs = new ArrayList<ItemParamData>(proudSkill.getCostItems());  // Can this be null?
-        if (proudSkill.getCoinCost() > 0) {
-            costs.add(new ItemParamData(202, proudSkill.getCoinCost()));
-        }
-        if (!player.getInventory().payItems(costs)) {
-            return;
-        }
-
-        // Upgrade skill
-        avatar.getSkillLevelMap().put(skillId, nextLevel);
-        avatar.save();
-
-        // Packet
-        player.sendPacket(new PacketAvatarSkillChangeNotify(avatar, skillId, currentLevel, nextLevel));
-        player.sendPacket(new PacketAvatarSkillUpgradeRsp(avatar, skillId, currentLevel, nextLevel));
+        avatar.upgradeSkill(skillId);
     }
 
+    @Deprecated(forRemoval = true)
     public void unlockAvatarConstellation(Player player, long guid) {
-        // Sanity checks
+        // Sanity check
         Avatar avatar = player.getAvatars().getAvatarByGuid(guid);
-        if (avatar == null) {
-            return;
-        }
+        if (avatar == null) return;
 
-        // Get talent
-        int currentTalentLevel = avatar.getCoreProudSkillLevel();
-        int nextTalentId = ((avatar.getAvatarId() % 10000000) * 10) + currentTalentLevel + 1;
-
-        if (avatar.getAvatarId() == 10000006) {
-            // Lisa is special in that her talentId starts with 4 instead of 6.
-            nextTalentId = 40 + currentTalentLevel + 1;
-        }
-
-        AvatarTalentData talentData = GameData.getAvatarTalentDataMap().get(nextTalentId);
-
-        if (talentData == null) {
-            return;
-        }
-
-        // Pay constellation item if possible
-        if (!player.getInventory().payItem(talentData.getMainCostItemId(), 1)) {
-            return;
-        }
-
-        // Apply + recalc
-        avatar.getTalentIdList().add(talentData.getId());
-        avatar.setCoreProudSkillLevel(currentTalentLevel + 1);
-
-        // Packet
-        player.sendPacket(new PacketAvatarUnlockTalentNotify(avatar, nextTalentId));
-        player.sendPacket(new PacketUnlockAvatarTalentRsp(avatar, nextTalentId));
-
-        // Proud skill bonus map (Extra skills)
-        OpenConfigEntry entry = GameData.getOpenConfigEntries().get(talentData.getOpenConfig());
-        if (entry != null) {
-            if (entry.getExtraTalentIndex() > 0) {
-                // Check if new constellation adds +3 to a skill level
-                avatar.recalcConstellations();
-                // Packet
-                player.sendPacket(new PacketProudSkillExtraLevelNotify(avatar, entry.getExtraTalentIndex()));
-            } else if (entry.getSkillPointModifiers() != null) {
-                // Check if new constellation adds skill charges
-                avatar.recalcConstellations();
-                // Packet
-                for (SkillPointModifier mod : entry.getSkillPointModifiers()) {
-                    player.sendPacket(
-                        new PacketAvatarSkillMaxChargeCountNotify(avatar, mod.getSkillId(), avatar.getSkillExtraChargeMap().getOrDefault(mod.getSkillId(), 0))
-                    );
-                }
-            }
-        }
-
-        // Recalc + save avatar
-        avatar.recalcStats(true);
-        avatar.save();
+        avatar.unlockConstellation();
     }
 
     public void destroyMaterial(Player player, List<MaterialInfo> list) {
diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java
index 1406c25b..8e6e05a9 100644
--- a/src/main/java/emu/grasscutter/game/world/Scene.java
+++ b/src/main/java/emu/grasscutter/game/world/Scene.java
@@ -5,6 +5,7 @@ import emu.grasscutter.data.GameData;
 import emu.grasscutter.data.GameDepot;
 import emu.grasscutter.data.binout.SceneNpcBornEntry;
 import emu.grasscutter.data.excels.*;
+import emu.grasscutter.game.avatar.Avatar;
 import emu.grasscutter.game.dungeons.DungeonSettleListener;
 import emu.grasscutter.game.entity.*;
 import emu.grasscutter.game.player.Player;
@@ -270,22 +271,19 @@ public class Scene {
     }
 
     public void spawnPlayer(Player player) {
-        if (this.isInScene(player.getTeamManager().getCurrentAvatarEntity())) {
+        var teamManager = player.getTeamManager();
+        if (this.isInScene(teamManager.getCurrentAvatarEntity())) {
             return;
         }
 
-        if (player.getTeamManager().getCurrentAvatarEntity().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
-            player.getTeamManager().getCurrentAvatarEntity().setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f);
+        if (teamManager.getCurrentAvatarEntity().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
+            teamManager.getCurrentAvatarEntity().setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f);
         }
 
-        this.addEntity(player.getTeamManager().getCurrentAvatarEntity());
+        this.addEntity(teamManager.getCurrentAvatarEntity());
 
         // Notify the client of any extra skill charges
-        for (EntityAvatar entity : player.getTeamManager().getActiveTeam()) {
-            if (entity.getAvatar().getSkillExtraChargeMap().size() > 0) {
-                player.sendPacket(new PacketAvatarSkillInfoNotify(entity.getAvatar()));
-            }
-        }
+        teamManager.getActiveTeam().stream().map(EntityAvatar::getAvatar).forEach(Avatar::sendSkillExtraChargeMap);
     }
 
     private void addEntityDirectly(GameEntity entity) {
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarChangeElementTypeReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarChangeElementTypeReq.java
index 4c6e4085..57d65200 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarChangeElementTypeReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarChangeElementTypeReq.java
@@ -19,50 +19,49 @@ import emu.grasscutter.server.packet.send.PacketAvatarSkillDepotChangeNotify;
 
 @Opcodes(PacketOpcodes.AvatarChangeElementTypeReq)
 public class HandlerAvatarChangeElementTypeReq extends PacketHandler {
-	
-	@Override
-	public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
-		AvatarChangeElementTypeReq req = AvatarChangeElementTypeReq.parseFrom(payload);
-		
-		WorldAreaData area = GameData.getWorldAreaDataMap().get(req.getAreaId());
-		
-		if (area == null || area.getElementType() == null || area.getElementType().getDepotValue() <= 0) {
-			session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE));
-			return;
-		}
-		
-		// Get current avatar, should be one of the main characters
-		EntityAvatar mainCharacterEntity = session.getPlayer().getTeamManager().getCurrentAvatarEntity();
-		
-		int intialSkillDepotId = 0;
-		if (mainCharacterEntity.getAvatar().getAvatarId() == GameConstants.MAIN_CHARACTER_MALE) {
-			intialSkillDepotId = 500;
-		} else if (mainCharacterEntity.getAvatar().getAvatarId() == GameConstants.MAIN_CHARACTER_FEMALE) {
-			intialSkillDepotId = 700;
-		} else {
-			session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE));
-			return;
-		}
-		intialSkillDepotId += area.getElementType().getDepotValue();
-		
-		// Sanity checks for skill depots
-		Avatar mainCharacter = mainCharacterEntity.getAvatar();
-		AvatarSkillDepotData skillDepot = GameData.getAvatarSkillDepotDataMap().get(intialSkillDepotId);
-		if (skillDepot == null || skillDepot.getId() == mainCharacter.getSkillDepotId()) {
-			session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE));
-			return;
-		}
-		
-		// Success
-		session.send(new PacketAvatarChangeElementTypeRsp());
-		
-		// Set skill depot
-		mainCharacter.setSkillDepotData(skillDepot);
-		
-		// Ability change packet
-		session.send(new PacketAvatarSkillDepotChangeNotify(mainCharacter));
-		session.send(new PacketAbilityChangeNotify(mainCharacterEntity));
-		session.send(new PacketAvatarFightPropNotify(mainCharacter));
-	}
+
+    @Override
+    public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+        AvatarChangeElementTypeReq req = AvatarChangeElementTypeReq.parseFrom(payload);
+
+        WorldAreaData area = GameData.getWorldAreaDataMap().get(req.getAreaId());
+
+        if (area == null || area.getElementType() == null || area.getElementType().getDepotValue() <= 0) {
+            session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE));
+            return;
+        }
+
+        // Get current avatar, should be one of the main characters
+        EntityAvatar mainCharacterEntity = session.getPlayer().getTeamManager().getCurrentAvatarEntity();
+        Avatar mainCharacter = mainCharacterEntity.getAvatar();
+
+        int skillDepotId = area.getElementType().getDepotValue();
+        switch (mainCharacter.getAvatarId()) {
+            case GameConstants.MAIN_CHARACTER_MALE -> skillDepotId += 500;
+            case GameConstants.MAIN_CHARACTER_FEMALE -> skillDepotId += 700;
+            default -> {
+                session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE));
+                return;
+            }
+        }
+
+        // Sanity checks for skill depots
+        AvatarSkillDepotData skillDepot = GameData.getAvatarSkillDepotDataMap().get(skillDepotId);
+        if (skillDepot == null || skillDepot.getId() == mainCharacter.getSkillDepotId()) {
+            session.send(new PacketAvatarChangeElementTypeRsp(Retcode.RET_SVR_ERROR_VALUE));
+            return;
+        }
+
+        // Set skill depot
+        mainCharacter.setSkillDepotData(skillDepot);
+
+        // Success
+        session.send(new PacketAvatarChangeElementTypeRsp());
+
+        // Ability change packet
+        session.send(new PacketAvatarSkillDepotChangeNotify(mainCharacter));
+        session.send(new PacketAbilityChangeNotify(mainCharacterEntity));
+        session.send(new PacketAvatarFightPropNotify(mainCharacter));
+    }
 
 }
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarSkillUpgradeReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarSkillUpgradeReq.java
index a371a8e8..c90b1a50 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarSkillUpgradeReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAvatarSkillUpgradeReq.java
@@ -13,8 +13,10 @@ public class HandlerAvatarSkillUpgradeReq extends PacketHandler {
     public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
         AvatarSkillUpgradeReq req = AvatarSkillUpgradeReq.parseFrom(payload);
 
+        // Sanity checks
+        var avatar = session.getPlayer().getAvatars().getAvatarByGuid(req.getAvatarGuid());
+        if (avatar == null) return;
         // Level up avatar talent
-        session.getServer().getInventorySystem().upgradeAvatarSkill(session.getPlayer(), req.getAvatarGuid(), req.getAvatarSkillId());
+        avatar.upgradeSkill(req.getAvatarSkillId());
     }
-
 }
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerUnlockAvatarTalentReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerUnlockAvatarTalentReq.java
index 5016017f..2723ed60 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerUnlockAvatarTalentReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerUnlockAvatarTalentReq.java
@@ -14,7 +14,9 @@ public class HandlerUnlockAvatarTalentReq extends PacketHandler {
         UnlockAvatarTalentReq req = UnlockAvatarTalentReq.parseFrom(payload);
 
         // Unlock avatar const
-        session.getServer().getInventorySystem().unlockAvatarConstellation(session.getPlayer(), req.getAvatarGuid());
+        var avatar = session.getPlayer().getAvatars().getAvatarByGuid(req.getAvatarGuid());
+        if (avatar == null) return;
+        avatar.unlockConstellation(req.getTalentId());
     }
 
 }
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarSkillInfoNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarSkillInfoNotify.java
index b099e564..f7fdba68 100644
--- a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarSkillInfoNotify.java
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarSkillInfoNotify.java
@@ -1,25 +1,20 @@
 package emu.grasscutter.server.packet.send;
 
-import java.util.Map.Entry;
-
-import emu.grasscutter.game.avatar.Avatar;
 import emu.grasscutter.net.packet.BasePacket;
 import emu.grasscutter.net.packet.PacketOpcodes;
 import emu.grasscutter.net.proto.AvatarSkillInfoNotifyOuterClass.AvatarSkillInfoNotify;
 import emu.grasscutter.net.proto.AvatarSkillInfoOuterClass.AvatarSkillInfo;
+import it.unimi.dsi.fastutil.ints.Int2IntMap;
 
 public class PacketAvatarSkillInfoNotify extends BasePacket {
-	
-	public PacketAvatarSkillInfoNotify(Avatar avatar) {
-		super(PacketOpcodes.AvatarSkillInfoNotify);
+    public PacketAvatarSkillInfoNotify(long avatarGuid, Int2IntMap skillExtraChargeMap) {
+        super(PacketOpcodes.AvatarSkillInfoNotify);
+
+        var proto = AvatarSkillInfoNotify.newBuilder().setGuid(avatarGuid);
+
+        skillExtraChargeMap.forEach((skillId, count) ->
+            proto.putSkillMap(skillId, AvatarSkillInfo.newBuilder().setMaxChargeCount(count).build()));
 
-		AvatarSkillInfoNotify.Builder proto = AvatarSkillInfoNotify.newBuilder()
-				.setGuid(avatar.getGuid());
-		
-		for (Entry<Integer, Integer> entry : avatar.getSkillExtraChargeMap().entrySet()) {
-			proto.putSkillMap(entry.getKey(), AvatarSkillInfo.newBuilder().setMaxChargeCount(entry.getValue()).build());
-		}
-		
-		this.setData(proto);
-	}
+        this.setData(proto);
+    }
 }
-- 
GitLab