diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index 3b99f1f2661..c47d0541c83 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -404,6 +404,10 @@ public final class GameData { private static final Int2ObjectMap weaponPromoteDataMap = new Int2ObjectOpenHashMap<>(); + @Getter + private static final Int2ObjectMap statuePromoteDataMap = + new Int2ObjectOpenHashMap<>(); + @Getter private static final Int2ObjectMap weatherDataMap = new Int2ObjectOpenHashMap<>(); @@ -567,6 +571,10 @@ public static WeaponPromoteData getWeaponPromoteData(int promoteId, int promoteL return weaponPromoteDataMap.get((promoteId << 8) + promoteLevel); } + public static StatuePromoteData getStatuePromoteData(int cityId, int promoteLevel) { + return statuePromoteDataMap.get((cityId << 8) + promoteLevel); + } + public static ReliquaryLevelData getRelicLevelData(int rankLevel, int level) { return reliquaryLevelDataMap.get((rankLevel << 8) + level); } diff --git a/src/main/java/emu/grasscutter/data/excels/StatuePromoteData.java b/src/main/java/emu/grasscutter/data/excels/StatuePromoteData.java new file mode 100644 index 00000000000..29e495a1cda --- /dev/null +++ b/src/main/java/emu/grasscutter/data/excels/StatuePromoteData.java @@ -0,0 +1,21 @@ +package emu.grasscutter.data.excels; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.data.common.ItemParamData; +import lombok.Getter; +import lombok.Setter; + +@ResourceType(name = "StatuePromoteExcelConfigData.json") +public class StatuePromoteData extends GameResource { + @Getter @Setter private int level; + @Getter @Setter private int cityId; + @Getter @Setter private ItemParamData[] costItems; + @Getter @Setter private int[] rewardIdList; + @Getter @Setter private int stamina; + + @Override + public int getId() { + return (cityId << 8) + level; + } +} diff --git a/src/main/java/emu/grasscutter/game/city/CityInfoData.java b/src/main/java/emu/grasscutter/game/city/CityInfoData.java new file mode 100644 index 00000000000..bc386bdec0b --- /dev/null +++ b/src/main/java/emu/grasscutter/game/city/CityInfoData.java @@ -0,0 +1,28 @@ +package emu.grasscutter.game.city; + +import dev.morphia.annotations.Entity; +import emu.grasscutter.net.proto.CityInfoOuterClass.CityInfo; +import lombok.Getter; +import lombok.Setter; + +@Entity +public class CityInfoData { + @Getter @Setter private int cityId; + + @Getter @Setter + private int level = 1; // level of the city (include level SotS, level Frostbearing Trees, etc.) + + @Getter @Setter private int numCrystal = 0; // number of crystals in the city + + public CityInfoData(int cityId) { + this.cityId = cityId; + } + + public CityInfo toProto() { + return CityInfo.newBuilder() + .setCityId(cityId) + .setLevel(level) + .setCrystalNum(numCrystal) + .build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/SotSManager.java b/src/main/java/emu/grasscutter/game/managers/SotSManager.java index 594e8cf5b76..3e05351e39f 100644 --- a/src/main/java/emu/grasscutter/game/managers/SotSManager.java +++ b/src/main/java/emu/grasscutter/game/managers/SotSManager.java @@ -2,15 +2,24 @@ import ch.qos.logback.classic.Logger; import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.CityData; +import emu.grasscutter.data.excels.RewardData; +import emu.grasscutter.game.city.CityInfoData; import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.player.BasePlayerManager; import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason; import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.server.packet.send.PacketLevelupCityRsp; +import emu.grasscutter.server.packet.send.PacketSceneForceUnlockNotify; +import java.util.HashMap; import java.util.List; import java.util.Timer; import java.util.TimerTask; @@ -208,4 +217,91 @@ public void run() { } } } + + public CityData getCityByAreaId(int areaId) { + return GameData.getCityDataMap().values().stream() + .filter(city -> city.getAreaIdVec().contains(areaId)) + .findFirst() + .orElse(null); + } + + public CityInfoData getCityInfo(int cityId) { + if (player.getCityInfoData() == null) player.setCityInfoData(new HashMap<>()); + var cityInfo = player.getCityInfoData().get(cityId); + if (cityInfo == null) { + cityInfo = new CityInfoData(cityId); + player.getCityInfoData().put(cityId, cityInfo); + } + return cityInfo; + } + + public void addCityInfo(CityInfoData cityInfoData) { + if (player.getCityInfoData() == null) player.setCityInfoData(new HashMap<>()); + + player.getCityInfoData().put(cityInfoData.getCityId(), cityInfoData); + } + + public void levelUpSotS(int areaId, int sceneId, int itemNum) { + if (itemNum <= 0) return; + + // search city by areaId + var city = this.getCityByAreaId(areaId); + if (city == null) return; + var cityId = city.getCityId(); + + // check data level up + var cityInfo = this.getCityInfo(cityId); + var nextStatuePromoteData = GameData.getStatuePromoteData(cityId, cityInfo.getLevel() + 1); + if (nextStatuePromoteData == null) return; + var nextLevelCrystal = nextStatuePromoteData.getCostItems()[0].getCount(); + + // delete item from inventory + var itemNumrequired = Math.min(itemNum, nextLevelCrystal - cityInfo.getNumCrystal()); + player + .getInventory() + .removeItemById(nextStatuePromoteData.getCostItems()[0].getId(), itemNumrequired); + + // update number oculi + cityInfo.setNumCrystal(cityInfo.getNumCrystal() + itemNumrequired); + + // hanble quest + if (itemNumrequired >= 1) + player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_CITY_LEVEL_UP, cityId, areaId); + + // handle oculi overflow + if (cityInfo.getNumCrystal() >= nextLevelCrystal) { + cityInfo.setNumCrystal(cityInfo.getNumCrystal() - nextLevelCrystal); + cityInfo.setLevel(cityInfo.getLevel() + 1); + + // update max stamina and notify client + player.setProperty( + PlayerProperty.PROP_MAX_STAMINA, + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + + nextStatuePromoteData.getStamina() * 100, + true); + + // Add items to inventory + if (nextStatuePromoteData.getRewardIdList() != null) { + for (var rewardId : nextStatuePromoteData.getRewardIdList()) { + RewardData rewardData = GameData.getRewardDataMap().get(rewardId); + if (rewardData == null) continue; + + player + .getInventory() + .addItemParamDatas(rewardData.getRewardItemList(), ActionReason.CityLevelupReward); + } + } + + // unlock forcescene + player.sendPacket(new PacketSceneForceUnlockNotify(1, true)); + } + + // update data + this.addCityInfo(cityInfo); + + // Packets + player.sendPacket( + new PacketLevelupCityRsp( + sceneId, cityInfo.getLevel(), cityId, cityInfo.getNumCrystal(), areaId, 0)); + } } diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 26e091bb296..6fce174364d 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -15,6 +15,7 @@ import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.battlepass.BattlePassManager; +import emu.grasscutter.game.city.CityInfoData; import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.expedition.ExpeditionInfo; import emu.grasscutter.game.friends.FriendsList; @@ -28,7 +29,6 @@ import emu.grasscutter.game.managers.FurnitureManager; import emu.grasscutter.game.managers.ResinManager; import emu.grasscutter.game.managers.SatiationManager; -import emu.grasscutter.game.managers.SotSManager; import emu.grasscutter.game.managers.cooking.ActiveCookCompoundData; import emu.grasscutter.game.managers.cooking.CookingCompoundManager; import emu.grasscutter.game.managers.cooking.CookingManager; @@ -38,6 +38,7 @@ import emu.grasscutter.game.managers.forging.ForgingManager; import emu.grasscutter.game.managers.mapmark.MapMark; import emu.grasscutter.game.managers.mapmark.MapMarksManager; +import emu.grasscutter.game.managers.SotSManager; import emu.grasscutter.game.managers.stamina.StaminaManager; import emu.grasscutter.game.props.*; import emu.grasscutter.game.quest.QuestManager; @@ -221,6 +222,8 @@ public class Player implements PlayerHook, FieldFetch { @Getter @Setter private ElementType mainCharacterElement = ElementType.None; + @Getter @Setter private Map cityInfoData; // cityId -> CityData + @Deprecated @SuppressWarnings({"rawtypes", "unchecked"}) // Morphia only! public Player() { @@ -267,6 +270,7 @@ public Player() { this.chatEmojiIdList = new ArrayList<>(); this.playerProgress = new PlayerProgress(); this.activeQuestTimers = new HashSet<>(); + this.cityInfoData = new HashMap<>(); this.attackResults = new LinkedBlockingQueue<>(); this.coopRequests = new Int2ObjectOpenHashMap<>(); @@ -1520,6 +1524,8 @@ private boolean setPropertyWithSanityCheck(PlayerProperty prop, int value, boole PropChangeReason.PROP_CHANGE_REASON_PLAYER_ADD_EXP)); case PROP_PLAYER_LEVEL -> this.sendPacket(new PacketPlayerPropChangeReasonNotify(this, prop, currentValue, value, PropChangeReason.PROP_CHANGE_REASON_LEVELUP)); + case PROP_MAX_STAMINA -> this.sendPacket(new PacketPlayerPropChangeReasonNotify(this, prop, currentValue, value, + PropChangeReason.PROP_CHANGE_REASON_CITY_LEVELUP)); // TODO: Handle world level changing. // case PROP_PLAYER_WORLD_LEVEL -> this.sendPacket(new PacketPlayerPropChangeReasonNotify(this, prop, currentValue, value, diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerLevelupCityReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerLevelupCityReq.java new file mode 100644 index 00000000000..74e61911d7d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerLevelupCityReq.java @@ -0,0 +1,22 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.LevelupCityReqOuterClass.LevelupCityReq; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.LevelupCityReq) +public class HandlerLevelupCityReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + LevelupCityReq req = LevelupCityReq.parseFrom(payload); + + // Level up city + session + .getPlayer() + .getSotsManager() + .levelUpSotS(req.getAreaId(), req.getSceneId(), req.getItemNum()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetSceneAreaRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetSceneAreaRsp.java index d7e3b32f9b5..09533776361 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetSceneAreaRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetSceneAreaRsp.java @@ -3,7 +3,6 @@ import emu.grasscutter.game.player.Player; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.CityInfoOuterClass.CityInfo; import emu.grasscutter.net.proto.GetSceneAreaRspOuterClass.GetSceneAreaRsp; public class PacketGetSceneAreaRsp extends BasePacket { @@ -17,9 +16,9 @@ public PacketGetSceneAreaRsp(Player player, int sceneId) { GetSceneAreaRsp.newBuilder() .setSceneId(sceneId) .addAllAreaIdList(player.getUnlockedSceneAreas(sceneId)) - .addCityInfoList(CityInfo.newBuilder().setCityId(1).setLevel(1).build()) - .addCityInfoList(CityInfo.newBuilder().setCityId(2).setLevel(1).build()) - .addCityInfoList(CityInfo.newBuilder().setCityId(3).setLevel(1).build()) + .addCityInfoList(player.getSotsManager().getCityInfo(1).toProto()) + .addCityInfoList(player.getSotsManager().getCityInfo(2).toProto()) + .addCityInfoList(player.getSotsManager().getCityInfo(3).toProto()) .build(); this.setData(p); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketLevelupCityRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketLevelupCityRsp.java new file mode 100644 index 00000000000..74cb7a988dd --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketLevelupCityRsp.java @@ -0,0 +1,29 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.CityInfoOuterClass.CityInfo; +import emu.grasscutter.net.proto.LevelupCityRspOuterClass.LevelupCityRsp; + +public class PacketLevelupCityRsp extends BasePacket { + + public PacketLevelupCityRsp( + int sceneId, int level, int cityId, int crystalNum, int areaId, int retcode) { + super(PacketOpcodes.LevelupCityRsp); + + LevelupCityRsp proto = + LevelupCityRsp.newBuilder() + .setSceneId(sceneId) + .setCityInfo( + CityInfo.newBuilder() + .setCityId(cityId) + .setLevel(level) + .setCrystalNum(crystalNum) + .build()) + .setAreaId(areaId) + .setRetcode(retcode) + .build(); + + this.setData(proto); + } +}