diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index f0dfca88c..d4de7f94d 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -1,5 +1,8 @@ package emu.grasscutter.game.gacha; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.game.player.Player; import emu.grasscutter.net.proto.GachaInfoOuterClass.GachaInfo; import emu.grasscutter.net.proto.GachaUpInfoOuterClass.GachaUpInfo; import emu.grasscutter.utils.Utils; @@ -44,6 +47,7 @@ public class GachaBanner { private int[] rateUpItems2 = {}; private int eventChance = -1; private int costItem = 0; + private int wishMaxProgress = 2; public int getGachaType() { return gachaType; @@ -108,6 +112,11 @@ public class GachaBanner { public boolean getRemoveC6FromPool() {return removeC6FromPool;} public boolean getAutoStripRateUpFromFallback() {return autoStripRateUpFromFallback;} + public int getWishMaxProgress() {return wishMaxProgress;} + + public boolean hasEpitomized() { + return bannerType.equals(BannerType.WEAPON); + } public int getWeight(int rarity, int pity) { return switch(rarity) { @@ -166,6 +175,17 @@ public class GachaBanner { .setLeftGachaTimes(Integer.MAX_VALUE) .setGachaTimesLimit(Integer.MAX_VALUE) .setGachaSortId(this.getSortId()); + + if(hasEpitomized() && !sessionKey.isEmpty()) { + Account account = DatabaseHelper.getAccountBySessionKey(sessionKey); + Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId()); + PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(this); + + info.setWishItemId(gachaInfo.getWishItemId()) + .setWishProgress(gachaInfo.getFailedChosenItemPulls()) + .setWishMaxProgress(this.getWishMaxProgress()); + } + if (this.getTitlePath() != null) { info.setTitleTextmap(this.getTitlePath()); } diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index 1e15eba12..84501d186 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -35,6 +35,7 @@ import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServerTickEvent; import emu.grasscutter.server.packet.send.PacketDoGachaRsp; +import emu.grasscutter.server.packet.send.PacketGachaWishRsp; import emu.grasscutter.utils.Utils; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -182,38 +183,59 @@ public class GachaManager { return 0; // This should only be reachable if total==0 } + private synchronized int doFallbackRarePull(int[] fallback1, int[] fallback2, int rarity, GachaBanner banner, PlayerGachaBannerInfo gachaInfo) { + if (fallback1.length < 1) { + if (fallback2.length < 1) { + return getRandom((rarity==5)? fallbackItems5Pool2Default : fallbackItems4Pool2Default); + } else { + return getRandom(fallback2); + } + } else if (fallback2.length < 1) { + return getRandom(fallback1); + } else { // Both pools are possible, use the pool balancer + int pityPool1 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 1)); + int pityPool2 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 2)); + int chosenPool = switch ((pityPool1 >= pityPool2)? 1 : 0) { // Larger weight must come first for the hard cutoff to function correctly + case 1 -> 1 + drawRoulette(new int[] {pityPool1, pityPool2}, 10000); + default -> 2 - drawRoulette(new int[] {pityPool2, pityPool1}, 10000); + }; + return switch (chosenPool) { + case 1: + gachaInfo.setPityPool(rarity, 1, 0); + yield getRandom(fallback1); + default: + gachaInfo.setPityPool(rarity, 2, 0); + yield getRandom(fallback2); + }; + } + } + private synchronized int doRarePull(int[] featured, int[] fallback1, int[] fallback2, int rarity, GachaBanner banner, PlayerGachaBannerInfo gachaInfo) { int itemId = 0; - boolean pullFeatured = (gachaInfo.getFailedFeaturedItemPulls(rarity) >= 1) // Lost previous coinflip - || (this.randomRange(1, 100) <= banner.getEventChance(rarity)); // Won this coinflip - if (pullFeatured && (featured.length > 0)) { - itemId = getRandom(featured); - gachaInfo.setFailedFeaturedItemPulls(rarity, 0); + boolean epitomized = (banner.hasEpitomized()) && (rarity == 5) && (gachaInfo.getWishItemId() != 0); + boolean pityEpitomized = (gachaInfo.getFailedChosenItemPulls() >= banner.getWishMaxProgress()); // Maximum fate points reached + boolean pityFeatured = (gachaInfo.getFailedFeaturedItemPulls(rarity) >= 1); // Lost previous coinflip + boolean rollFeatured = (this.randomRange(1, 100) <= banner.getEventChance(rarity)); // Won this coinflip + boolean pullFeatured = pityFeatured || rollFeatured; + + if (epitomized && pityEpitomized) { // Auto pick item when epitomized points reached + gachaInfo.setFailedFeaturedItemPulls(rarity, 0); // Epitomized item will always be a featured one + itemId = gachaInfo.getWishItemId(); } else { - gachaInfo.addFailedFeaturedItemPulls(rarity, 1); - if (fallback1.length < 1) { - if (fallback2.length < 1) { - itemId = getRandom((rarity==5)? fallbackItems5Pool2Default : fallbackItems4Pool2Default); - } else { - itemId = getRandom(fallback2); - } - } else if (fallback2.length < 1) { - itemId = getRandom(fallback1); - } else { // Both pools are possible, use the pool balancer - int pityPool1 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 1)); - int pityPool2 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 2)); - int chosenPool = switch ((pityPool1 >= pityPool2)? 1 : 0) { // Larger weight must come first for the hard cutoff to function correctly - case 1 -> 1 + drawRoulette(new int[] {pityPool1, pityPool2}, 10000); - default -> 2 - drawRoulette(new int[] {pityPool2, pityPool1}, 10000); - }; - itemId = switch (chosenPool) { - case 1: - gachaInfo.setPityPool(rarity, 1, 0); - yield getRandom(fallback1); - default: - gachaInfo.setPityPool(rarity, 2, 0); - yield getRandom(fallback2); - }; + if (pullFeatured && (featured.length > 0)) { + gachaInfo.setFailedFeaturedItemPulls(rarity, 0); + itemId = getRandom(featured); + } else { + gachaInfo.addFailedFeaturedItemPulls(rarity, 1); // This could be moved into doFallbackRarePull but having it here makes it clearer + itemId = doFallbackRarePull(fallback1, fallback2, rarity, banner, gachaInfo); + } + } + + if (epitomized) { + if(itemId == gachaInfo.getWishItemId()) { // Reset epitomized points when got wished item + gachaInfo.setFailedChosenItemPulls(0); + } else { // Add epitomized points if not get wished item + gachaInfo.addFailedChosenItemPulls(1); } } return itemId; @@ -356,7 +378,7 @@ public class GachaManager { } // Packets - player.sendPacket(new PacketDoGachaRsp(banner, list)); + player.sendPacket(new PacketDoGachaRsp(banner, list, gachaInfo)); } private synchronized void startWatcher(GameServer server) { diff --git a/src/main/java/emu/grasscutter/game/gacha/PlayerGachaBannerInfo.java b/src/main/java/emu/grasscutter/game/gacha/PlayerGachaBannerInfo.java index f07d2eff0..00b07ed96 100644 --- a/src/main/java/emu/grasscutter/game/gacha/PlayerGachaBannerInfo.java +++ b/src/main/java/emu/grasscutter/game/gacha/PlayerGachaBannerInfo.java @@ -12,6 +12,9 @@ public class PlayerGachaBannerInfo { private int pity5Pool2 = 0; private int pity4Pool1 = 0; private int pity4Pool2 = 0; + + private int failedChosenItemPulls = 0; + private int wishItemId = 0; public int getPity5() { return pity5; @@ -36,6 +39,26 @@ public class PlayerGachaBannerInfo { public void addPity4(int amount) { this.pity4 += amount; } + + public int getWishItemId() { + return wishItemId; + } + + public void setWishItemId(int wishItemId) { + this.wishItemId = wishItemId; + } + + public int getFailedChosenItemPulls() { + return failedChosenItemPulls; + } + + public void setFailedChosenItemPulls(int amount) { + failedChosenItemPulls = amount; + } + + public void addFailedChosenItemPulls(int amount) { + failedChosenItemPulls += amount; + } public int getFailedFeaturedItemPulls(int rarity) { return switch (rarity) { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGachaWishReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGachaWishReq.java new file mode 100644 index 000000000..ce3159dc5 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGachaWishReq.java @@ -0,0 +1,28 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.gacha.GachaBanner; +import emu.grasscutter.game.gacha.PlayerGachaBannerInfo; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.GachaWishReqOuterClass.GachaWishReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketGachaWishRsp; + +@Opcodes(PacketOpcodes.GachaWishReq) +public class HandlerGachaWishReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + GachaWishReq req = GachaWishReq.parseFrom(payload); + + GachaBanner banner = session.getServer().getGachaManager().getGachaBanners().get(req.getGachaScheduleId()); + PlayerGachaBannerInfo gachaInfo = session.getPlayer().getGachaInfo().getBannerInfo(banner); + + gachaInfo.setFailedChosenItemPulls(0); + gachaInfo.setWishItemId(req.getItemId()); + + session.send(new PacketGachaWishRsp(req.getGachaType(), req.getGachaScheduleId(), req.getItemId(), 0, banner.getWishMaxProgress())); + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetGachaInfoReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetGachaInfoReq.java index 76d267b99..cb738a085 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetGachaInfoReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetGachaInfoReq.java @@ -1,9 +1,12 @@ package emu.grasscutter.server.packet.recv; +import emu.grasscutter.game.gacha.GachaBanner; +import emu.grasscutter.game.gacha.PlayerGachaBannerInfo; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketGachaWishRsp; import emu.grasscutter.server.packet.send.PacketGetGachaInfoRsp; @Opcodes(PacketOpcodes.GetGachaInfoReq) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDoGachaRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDoGachaRsp.java index db63a7169..f63e053b6 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketDoGachaRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDoGachaRsp.java @@ -4,6 +4,7 @@ import java.util.List; import emu.grasscutter.data.common.ItemParamData; import emu.grasscutter.game.gacha.GachaBanner; +import emu.grasscutter.game.gacha.PlayerGachaBannerInfo; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.DoGachaRspOuterClass.DoGachaRsp; @@ -13,12 +14,12 @@ import emu.grasscutter.net.proto.RetcodeOuterClass; public class PacketDoGachaRsp extends BasePacket { - public PacketDoGachaRsp(GachaBanner banner, List list) { + public PacketDoGachaRsp(GachaBanner banner, List list, PlayerGachaBannerInfo gachaInfo) { super(PacketOpcodes.DoGachaRsp); ItemParamData costItem = banner.getCost(1); ItemParamData costItem10 = banner.getCost(10); - DoGachaRsp p = DoGachaRsp.newBuilder() + DoGachaRsp.Builder rsp = DoGachaRsp.newBuilder() .setGachaType(banner.getGachaType()) .setGachaScheduleId(banner.getScheduleId()) .setGachaTimes(list.size()) @@ -28,10 +29,15 @@ public class PacketDoGachaRsp extends BasePacket { .setCostItemNum(costItem.getCount()) .setTenCostItemId(costItem10.getId()) .setTenCostItemNum(costItem10.getCount()) - .addAllGachaItemList(list) - .build(); + .addAllGachaItemList(list); + + if(banner.hasEpitomized()) { + rsp.setWishItemId(gachaInfo.getWishItemId()) + .setWishProgress(gachaInfo.getFailedChosenItemPulls()) + .setWishMaxProgress(banner.getWishMaxProgress()); + } - this.setData(p); + this.setData(rsp.build()); } public PacketDoGachaRsp() { diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGachaWishRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGachaWishRsp.java new file mode 100644 index 000000000..713f25caa --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGachaWishRsp.java @@ -0,0 +1,23 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.GachaWishRspOuterClass.GachaWishRsp; + +public class PacketGachaWishRsp extends BasePacket { + + public PacketGachaWishRsp(int gachaType, int scheduleId, int itemId, int progress, int maxProgress) { + super(PacketOpcodes.GachaWishRsp); + + GachaWishRsp proto = GachaWishRsp.newBuilder() + .setGachaType(gachaType) + .setGachaScheduleId(scheduleId) + .setWishItemId(itemId) + .setWishProgress(progress) + .setWishMaxProgress(maxProgress) + .build(); + + this.setData(proto); + } + +}