Gacha rework

Add fallback stripping and C6 stripping
Converting banner definitions from pity vars to lerp arrays
Properly implement rates and pool smoothing
Also move reusable functions to Utils
This commit is contained in:
AnimeGitB 2022-05-06 23:39:45 +09:30 committed by Melledy
parent 6c6e206a88
commit 3f3ab11ef6
5 changed files with 397 additions and 168 deletions

View File

@ -10,8 +10,9 @@
"beginTime": 0, "beginTime": 0,
"endTime": 1924992000, "endTime": 1924992000,
"sortId": 1000, "sortId": 1000,
"rateUpItems1": [], "fallbackItems4Pool1": [1006, 1014, 1015, 1020, 1021, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064],
"rateUpItems2": [] "weights4": [[1,510], [8,510], [10,10000]],
"weights5": [[1,75], [73,150], [90,10000]]
}, },
{ {
"gachaType": 301, "gachaType": 301,
@ -24,9 +25,10 @@
"beginTime": 0, "beginTime": 0,
"endTime": 1924992000, "endTime": 1924992000,
"sortId": 9998, "sortId": 9998,
"maxItemType": 1, "rateUpItems4": [1053, 1020, 1045],
"rateUpItems1": [1002], "rateUpItems5": [1002],
"rateUpItems2": [1053, 1020, 1045] "fallbackItems5Pool2": [],
"weights5": [[1,80], [73,80], [90,10000]]
}, },
{ {
"gachaType": 302, "gachaType": 302,
@ -39,11 +41,12 @@
"beginTime": 0, "beginTime": 0,
"endTime": 1924992000, "endTime": 1924992000,
"sortId": 9997, "sortId": 9997,
"minItemType": 2,
"eventChance": 75, "eventChance": 75,
"softPity": 80, "softPity": 80,
"hardPity": 80, "hardPity": 80,
"rateUpItems1": [11509, 12504], "rateUpItems4": [11401, 12402, 13407, 14401, 15401],
"rateUpItems2": [11401, 12402, 13407, 14401, 15401] "rateUpItems5": [11509, 12504],
"fallbackItems5Pool1": [],
"weights5": [[1,100], [62,100], [73, 7800], [80,10000]]
} }
] ]

View File

@ -2,6 +2,7 @@ package emu.grasscutter.game.gacha;
import emu.grasscutter.net.proto.GachaInfoOuterClass.GachaInfo; import emu.grasscutter.net.proto.GachaInfoOuterClass.GachaInfo;
import emu.grasscutter.net.proto.GachaUpInfoOuterClass.GachaUpInfo; import emu.grasscutter.net.proto.GachaUpInfoOuterClass.GachaUpInfo;
import emu.grasscutter.utils.Utils;
import static emu.grasscutter.Configuration.*; import static emu.grasscutter.Configuration.*;
@ -15,15 +16,32 @@ public class GachaBanner {
private int beginTime; private int beginTime;
private int endTime; private int endTime;
private int sortId; private int sortId;
private int[] rateUpItems1; private int[] rateUpItems4 = {};
private int[] rateUpItems2; private int[] rateUpItems5 = {};
private int baseYellowWeight = 60; // Max 10000 private int[] fallbackItems3 = {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304};
private int basePurpleWeight = 510; // Max 10000 private int[] fallbackItems4Pool1 = {1014, 1020, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064};
private int eventChance = 50; // Chance to win a featured event item private int[] fallbackItems4Pool2 = {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405};
private int softPity = 75; private int[] fallbackItems5Pool1 = {1003, 1016, 1042, 1035, 1041};
private int hardPity = 90; private int[] fallbackItems5Pool2 = {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
private boolean removeC6FromPool = false;
private boolean autoStripRateUpFromFallback = true;
private int[][] weights4 = {{1,510}, {8,510}, {10,10000}};
private int[][] weights5 = {{1,75}, {73,150}, {90,10000}};
private int[][] poolBalanceWeights4 = {{1,255}, {17,255}, {21,10455}};
private int[][] poolBalanceWeights5 = {{1,30}, {147,150}, {181,10230}};
private int eventChance4 = 50; // Chance to win a featured event item
private int eventChance5 = 50; // Chance to win a featured event item
private BannerType bannerType = BannerType.STANDARD; private BannerType bannerType = BannerType.STANDARD;
// Kinda wanna deprecate these but they're in people's configs
private int[] rateUpItems1 = {};
private int[] rateUpItems2 = {};
private int softPity = -1;
private int hardPity = -1;
private int eventChance = -1;
private int baseYellowWeight = -1;
private int basePurpleWeight = -1;
public int getGachaType() { public int getGachaType() {
return gachaType; return gachaType;
} }
@ -72,24 +90,42 @@ public class GachaBanner {
return basePurpleWeight; return basePurpleWeight;
} }
public int[] getRateUpItems1() { public int[] getRateUpItems4() {
return rateUpItems1; return (rateUpItems2.length > 0) ? rateUpItems2 : rateUpItems4;
}
public int[] getRateUpItems5() {
return (rateUpItems1.length > 0) ? rateUpItems1 : rateUpItems5;
} }
public int[] getRateUpItems2() { public int[] getFallbackItems3() {return fallbackItems3;}
return rateUpItems2; public int[] getFallbackItems4Pool1() {return fallbackItems4Pool1;}
public int[] getFallbackItems4Pool2() {return fallbackItems4Pool2;}
public int[] getFallbackItems5Pool1() {return fallbackItems5Pool1;}
public int[] getFallbackItems5Pool2() {return fallbackItems5Pool2;}
public boolean getRemoveC6FromPool() {return removeC6FromPool;}
public boolean getAutoStripRateUpFromFallback() {return autoStripRateUpFromFallback;}
public int getWeight(int rarity, int pity) {
return switch(rarity) {
case 4 -> Utils.lerp(pity, weights4);
default -> Utils.lerp(pity, weights5);
};
} }
public int getSoftPity() { public int getPoolBalanceWeight(int rarity, int pity) {
return softPity - 1; return switch(rarity) {
case 4 -> Utils.lerp(pity, poolBalanceWeights4);
default -> Utils.lerp(pity, poolBalanceWeights5);
};
} }
public int getHardPity() { public int getEventChance(int rarity) {
return hardPity - 1; return switch(rarity) {
} case 4 -> eventChance4;
default -> (eventChance > -1) ? eventChance : eventChance5;
public int getEventChance() { };
return eventChance;
} }
@Deprecated @Deprecated
@ -131,10 +167,10 @@ public class GachaBanner {
info.setGachaTitlePath(this.getTitlePath()); info.setGachaTitlePath(this.getTitlePath());
} }
if (this.getRateUpItems1().length > 0) { if (this.getRateUpItems5().length > 0) {
GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(1); GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(1);
for (int id : getRateUpItems1()) { for (int id : getRateUpItems5()) {
upInfo.addItemIdList(id); upInfo.addItemIdList(id);
info.addMainNameId(id); info.addMainNameId(id);
} }
@ -142,10 +178,10 @@ public class GachaBanner {
info.addGachaUpInfoList(upInfo); info.addGachaUpInfoList(upInfo);
} }
if (this.getRateUpItems2().length > 0) { if (this.getRateUpItems4().length > 0) {
GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(2); GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(2);
for (int id : getRateUpItems2()) { for (int id : getRateUpItems4()) {
upInfo.addItemIdList(id); upInfo.addItemIdList(id);
if (info.getSubNameIdCount() == 0) { if (info.getSubNameIdCount() == 0) {
info.addSubNameId(id); info.addSubNameId(id);

View File

@ -29,6 +29,7 @@ import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.game.GameServerTickEvent; import emu.grasscutter.server.game.GameServerTickEvent;
import emu.grasscutter.server.packet.send.PacketDoGachaRsp; import emu.grasscutter.server.packet.send.PacketDoGachaRsp;
import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntArrayList;
@ -43,12 +44,6 @@ public class GachaManager {
private GetGachaInfoRsp cachedProto; private GetGachaInfoRsp cachedProto;
WatchService watchService; WatchService watchService;
private final int[] yellowAvatars = new int[] {1003, 1016, 1042, 1035, 1041};
private final int[] yellowWeapons = new int[] {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
private final int[] purpleAvatars = new int[] {1006, 1014, 1015, 1020, 1021, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064};
private final int[] purpleWeapons = new int[] {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405};
private final int[] blueWeapons = new int[] {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304};
private static final int starglitterId = 221; private static final int starglitterId = 221;
private static final int stardustId = 222; private static final int stardustId = 222;
@ -67,23 +62,7 @@ public class GachaManager {
return gachaBanners; return gachaBanners;
} }
public int[] getYellowAvatars() { public int randomRange(int min, int max) { // Both are inclusive
return this.yellowAvatars;
}
public int[] getYellowWeapons() {
return this.yellowWeapons;
}
public int[] getPurpleAvatars() {
return this.purpleAvatars;
}
public int[] getPurpleWeapons() {
return this.purpleWeapons;
}
public int[] getBlueWeapons() {
return this.blueWeapons;
}
public int randomRange(int min, int max) {
return ThreadLocalRandom.current().nextInt(max - min + 1) + min; return ThreadLocalRandom.current().nextInt(max - min + 1) + min;
} }
@ -98,8 +77,14 @@ public class GachaManager {
if(banners.size() > 0) { if(banners.size() > 0) {
for (GachaBanner banner : banners) { for (GachaBanner banner : banners) {
getGachaBanners().put(banner.getGachaType(), banner); getGachaBanners().put(banner.getGachaType(), banner);
Grasscutter.getLogger().info(String.format("Testing lerp code for banner gachaType %d :", banner.getGachaType())); // TODO: remove this before merging!
for (int i=1; i<91; i++) {
Grasscutter.getLogger().info(String.format("Pity %d : Weight %d", i, banner.getWeight(5, i)));
}
} }
Grasscutter.getLogger().info("Banners successfully loaded."); Grasscutter.getLogger().info("Banners successfully loaded.");
this.cachedProto = createProto(); this.cachedProto = createProto();
} else { } else {
Grasscutter.getLogger().error("Unable to load banners. Banners size is 0."); Grasscutter.getLogger().error("Unable to load banners. Banners size is 0.");
@ -110,6 +95,139 @@ public class GachaManager {
} }
} }
private class BannerPools {
public int[] rateUpItems4;
public int[] rateUpItems5;
public int[] fallbackItems4Pool1;
public int[] fallbackItems4Pool2;
public int[] fallbackItems5Pool1;
public int[] fallbackItems5Pool2;
public BannerPools(GachaBanner banner) {
rateUpItems4 = banner.getRateUpItems4();
rateUpItems5 = banner.getRateUpItems5();
fallbackItems4Pool1 = banner.getFallbackItems4Pool1();
fallbackItems4Pool2 = banner.getFallbackItems4Pool2();
fallbackItems5Pool1 = banner.getFallbackItems5Pool1();
fallbackItems5Pool2 = banner.getFallbackItems5Pool2();
if (banner.getAutoStripRateUpFromFallback()) {
fallbackItems4Pool1 = Utils.setSubtract(fallbackItems4Pool1, rateUpItems4);
fallbackItems4Pool2 = Utils.setSubtract(fallbackItems4Pool2, rateUpItems4);
fallbackItems5Pool1 = Utils.setSubtract(fallbackItems5Pool1, rateUpItems5);
fallbackItems5Pool2 = Utils.setSubtract(fallbackItems5Pool2, rateUpItems5);
}
}
public void removeFromAllPools(int[] itemIds) {
rateUpItems4 = Utils.setSubtract(rateUpItems4, itemIds);
rateUpItems5 = Utils.setSubtract(rateUpItems5, itemIds);
fallbackItems4Pool1 = Utils.setSubtract(fallbackItems4Pool1, itemIds);
fallbackItems4Pool2 = Utils.setSubtract(fallbackItems4Pool2, itemIds);
fallbackItems5Pool1 = Utils.setSubtract(fallbackItems5Pool1, itemIds);
fallbackItems5Pool2 = Utils.setSubtract(fallbackItems5Pool2, itemIds);
}
}
private synchronized int checkPlayerAvatarConstellationLevel(Player player, int itemId) { // Maybe this would be useful in the Player class?
ItemData itemData = GameData.getItemDataMap().get(itemId);
if ((itemData == null) || (itemData.getMaterialType() != MaterialType.MATERIAL_AVATAR)){
return -2; // Not an Avatar
}
Avatar avatar = player.getAvatars().getAvatarById((itemId % 1000) + 10000000);
if (avatar == null) {
return -1; // Doesn't have
}
// Constellation
int constLevel = avatar.getCoreProudSkillLevel();
GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId + 100);
constLevel += (constItem == null)? 0 : constItem.getCount();
return constLevel;
}
private synchronized int[] removeC6FromPool(int[] itemPool, Player player) {
IntList temp = new IntArrayList();
for (int itemId : itemPool) {
if (checkPlayerAvatarConstellationLevel(player, itemId) < 6) {
temp.add(itemId);
}
}
return temp.toIntArray();
}
private synchronized int drawRoulette(int[] weights, int cutoff) {
// This follows the logic laid out in issue #183
// Simple weighted selection with an upper bound for the roll that cuts off trailing entries
// All weights must be >= 0
int total = 0;
for (int i : weights) {
if (i < 0) {
throw new IllegalArgumentException("Weights must be non-negative!");
}
total += i;
}
int roll = ThreadLocalRandom.current().nextInt((total < cutoff)? total : cutoff);
int subTotal = 0;
for (int i : weights) {
subTotal += i;
if (roll < subTotal) {
return i;
}
}
// throw new IllegalStateException();
return 0; // This should only be reachable if total==0
}
private synchronized int doRarePull(int[] featured, int[] fallback1, int[] fallback2, int rarity, GachaBanner banner, PlayerGachaBannerInfo gachaInfo) {
int itemId = 0;
if ( (featured.length > 0)
&& (gachaInfo.getFailedFeaturedItemPulls(rarity) >= 1)
|| (this.randomRange(1, 100) <= banner.getEventChance(rarity))) {
itemId = getRandom(featured);
gachaInfo.setFailedFeaturedItemPulls(rarity, 0);
} else {
gachaInfo.addFailedFeaturedItemPulls(rarity, 1);
if (fallback1.length < 1) {
itemId = getRandom(fallback2); // Don't ever run an empty fallback2 btw
} else {
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);
};
}
}
return itemId;
}
private synchronized int doPull(GachaBanner banner, PlayerGachaBannerInfo gachaInfo, BannerPools pools) {
// Pre-increment all pity pools (yes this makes all calculations assume 1-indexed pity)
gachaInfo.incPityAll();
int[] weights = {banner.getWeight(5, gachaInfo.getPity5()), banner.getWeight(4, gachaInfo.getPity4()), 10000};
int levelWon = 5 - drawRoulette(weights, 10000);
return switch (levelWon) {
case 5:
gachaInfo.setPity5(0);
yield doRarePull(pools.rateUpItems5, pools.fallbackItems5Pool1, pools.fallbackItems5Pool2, 5, banner, gachaInfo);
case 4:
gachaInfo.setPity4(0);
yield doRarePull(pools.rateUpItems4, pools.fallbackItems4Pool1, pools.fallbackItems4Pool2, 4, banner, gachaInfo);
default:
yield getRandom(banner.getFallbackItems3());
};
}
public synchronized void doPulls(Player player, int gachaType, int times) { public synchronized void doPulls(Player player, int gachaType, int times) {
// Sanity check // Sanity check
if (times != 10 && times != 1) { if (times != 10 && times != 1) {
@ -132,84 +250,27 @@ public class GachaManager {
return; return;
} }
// Roll
PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner);
IntList wonItems = new IntArrayList(times);
for (int i = 0; i < times; i++) {
int random = this.randomRange(1, 10000);
int itemId = 0;
int bonusYellowChance = gachaInfo.getPity5() >= banner.getSoftPity() ? 100 * (gachaInfo.getPity5() - banner.getSoftPity() - 1): 0;
int yellowChance = banner.getBaseYellowWeight() + (int) Math.floor(100f * (gachaInfo.getPity5() / (banner.getSoftPity() - 1D))) + bonusYellowChance;
int purpleChance = 10000 - (banner.getBasePurpleWeight() + (int) Math.floor(790f * (gachaInfo.getPity4() / 8f)));
if (random <= yellowChance || gachaInfo.getPity5() >= banner.getHardPity()) {
if (banner.getRateUpItems1().length > 0) {
int eventChance = this.randomRange(1, 100);
if (eventChance <= banner.getEventChance() || gachaInfo.getFailedFeaturedItemPulls() >= 1) {
itemId = getRandom(banner.getRateUpItems1());
gachaInfo.setFailedFeaturedItemPulls(0);
} else {
// Lost the 50/50... rip
gachaInfo.addFailedFeaturedItemPulls(1);
}
}
if (itemId == 0) {
int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2);
if (typeChance == 1) {
itemId = getRandom(this.yellowAvatars);
} else {
itemId = getRandom(this.yellowWeapons);
}
}
// Pity
gachaInfo.addPity4(1);
gachaInfo.setPity5(0);
} else if (random >= purpleChance || gachaInfo.getPity4() >= 9) {
if (banner.getRateUpItems2().length > 0) {
int eventChance = this.randomRange(1, 100);
if (eventChance >= 50) {
itemId = getRandom(banner.getRateUpItems2());
}
}
if (itemId == 0) {
int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2);
if (typeChance == 1) {
itemId = getRandom(this.purpleAvatars);
} else {
itemId = getRandom(this.purpleWeapons);
}
}
// Pity
gachaInfo.addPity5(1);
gachaInfo.setPity4(0);
} else {
itemId = getRandom(this.blueWeapons);
// Pity
gachaInfo.addPity4(1);
gachaInfo.addPity5(1);
}
// Add winning item
wonItems.add(itemId);
}
// Add to character // Add to character
PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner);
BannerPools pools = new BannerPools(banner);
List<GachaItem> list = new ArrayList<>(); List<GachaItem> list = new ArrayList<>();
int stardust = 0, starglitter = 0; int stardust = 0, starglitter = 0;
for (int itemId : wonItems) { if (banner.getRemoveC6FromPool()) { // The ultimate form of pity (non-vanilla)
pools.rateUpItems4 = removeC6FromPool(pools.rateUpItems4, player);
pools.rateUpItems5 = removeC6FromPool(pools.rateUpItems5, player);
pools.fallbackItems4Pool1 = removeC6FromPool(pools.fallbackItems4Pool1, player);
pools.fallbackItems4Pool2 = removeC6FromPool(pools.fallbackItems4Pool2, player);
pools.fallbackItems5Pool1 = removeC6FromPool(pools.fallbackItems5Pool1, player);
pools.fallbackItems5Pool2 = removeC6FromPool(pools.fallbackItems5Pool2, player);
}
for (int i = 0; i < times; i++) {
// Roll
int itemId = doPull(banner, gachaInfo, pools);
ItemData itemData = GameData.getItemDataMap().get(itemId); ItemData itemData = GameData.getItemDataMap().get(itemId);
if (itemData == null) { if (itemData == null) {
continue; continue; // Maybe we should bail out if an item fails instead of rolling the rest?
} }
// Write gacha record // Write gacha record
@ -222,44 +283,33 @@ public class GachaManager {
boolean isTransferItem = false; boolean isTransferItem = false;
// Const check // Const check
if (itemData.getMaterialType() == MaterialType.MATERIAL_AVATAR) { int constellation = checkPlayerAvatarConstellationLevel(player, itemId);
int avatarId = (itemData.getId() % 1000) + 10000000; switch (constellation) {
Avatar avatar = player.getAvatars().getAvatarById(avatarId); case -2: // Is weapon
if (avatar != null) { switch (itemData.getRankLevel()) {
int constLevel = avatar.getCoreProudSkillLevel(); case 5 -> addStarglitter = 10;
int constItemId = itemData.getId() + 100; case 4 -> addStarglitter = 2;
GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId); default -> addStardust = 15;
if (constItem != null) {
constLevel += constItem.getCount();
} }
break;
if (constLevel < 6) { case -1: // New character
// Not max const gachaItem.setIsGachaItemNew(true);
addStarglitter = 2; break;
// Add 1 const default:
if (constellation >= 6) { // C6, give consolation starglitter
addStarglitter = (itemData.getRankLevel()==5)? 25 : 5;
} else { // C0-C5, give constellation item
if (banner.getRemoveC6FromPool() && constellation == 5) { // New C6, remove it from the pools so we don't get C7 in a 10pull
pools.removeFromAllPools(new int[] {itemId});
}
addStarglitter = (itemData.getRankLevel()==5)? 10 : 2;
int constItemId = itemId + 100;
GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId);
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null)); gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null));
player.getInventory().addItem(constItemId, 1); player.getInventory().addItem(constItemId, 1);
} else {
// Is max const
addStarglitter = 5;
} }
if (itemData.getRankLevel() == 5) {
addStarglitter *= 5;
}
isTransferItem = true; isTransferItem = true;
} else { break;
// New
gachaItem.setIsGachaItemNew(true);
}
} else {
// Is weapon
switch (itemData.getRankLevel()) {
case 5 -> addStarglitter = 10;
case 4 -> addStarglitter = 2;
case 3 -> addStardust = 15;
}
} }
// Create item // Create item
@ -272,7 +322,8 @@ public class GachaManager {
if (addStardust > 0) { if (addStardust > 0) {
gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(stardustId).setCount(addStardust)); gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(stardustId).setCount(addStardust));
} if (addStarglitter > 0) { }
if (addStarglitter > 0) {
ItemParam starglitterParam = ItemParam.newBuilder().setItemId(starglitterId).setCount(addStarglitter).build(); ItemParam starglitterParam = ItemParam.newBuilder().setItemId(starglitterId).setCount(addStarglitter).build();
if (isTransferItem) { if (isTransferItem) {
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(starglitterParam)); gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(starglitterParam));
@ -286,7 +337,8 @@ public class GachaManager {
// Add stardust/starglitter // Add stardust/starglitter
if (stardust > 0) { if (stardust > 0) {
player.getInventory().addItem(stardustId, stardust); player.getInventory().addItem(stardustId, stardust);
} if (starglitter > 0) { }
if (starglitter > 0) {
player.getInventory().addItem(starglitterId, starglitter); player.getInventory().addItem(starglitterId, starglitter);
} }

View File

@ -7,6 +7,11 @@ public class PlayerGachaBannerInfo {
private int pity5 = 0; private int pity5 = 0;
private int pity4 = 0; private int pity4 = 0;
private int failedFeaturedItemPulls = 0; private int failedFeaturedItemPulls = 0;
private int failedFeatured4ItemPulls = 0;
private int pity5Pool1 = 0;
private int pity5Pool2 = 0;
private int pity4Pool1 = 0;
private int pity4Pool2 = 0;
public int getPity5() { public int getPity5() {
return pity5; return pity5;
@ -32,15 +37,82 @@ public class PlayerGachaBannerInfo {
this.pity4 += amount; this.pity4 += amount;
} }
public int getFailedFeaturedItemPulls() { public int getFailedFeaturedItemPulls(int rarity) {
return failedFeaturedItemPulls; return switch (rarity) {
case 4 -> failedFeatured4ItemPulls;
default -> failedFeaturedItemPulls; // 5
};
} }
public void setFailedFeaturedItemPulls(int failedEventCharacterPulls) { public void setFailedFeaturedItemPulls(int rarity, int amount) {
this.failedFeaturedItemPulls = failedEventCharacterPulls; switch (rarity) {
case 4 -> failedFeatured4ItemPulls = amount;
default -> failedFeaturedItemPulls = amount; // 5
};
} }
public void addFailedFeaturedItemPulls(int amount) { public void addFailedFeaturedItemPulls(int rarity, int amount) {
failedFeaturedItemPulls += amount; switch (rarity) {
case 4 -> failedFeatured4ItemPulls += amount;
default -> failedFeaturedItemPulls += amount; // 5
};
}
public int getPityPool(int rarity, int pool) {
return switch (rarity) {
case 4 -> switch (pool) {
case 1 -> pity4Pool1;
default -> pity4Pool2;
};
default -> switch (pool) {
case 1 -> pity5Pool1;
default -> pity5Pool2;
};
};
}
public void setPityPool(int rarity, int pool, int amount) {
switch (rarity) {
case 4:
switch (pool) {
case 1 -> pity4Pool1 = amount;
default -> pity4Pool2 = amount;
};
break;
case 5:
default:
switch (pool) {
case 1 -> pity5Pool1 = amount;
default -> pity5Pool2 = amount;
};
break;
};
}
public void addPityPool(int rarity, int pool, int amount) {
switch (rarity) {
case 4:
switch (pool) {
case 1 -> pity4Pool1 += amount;
default -> pity4Pool2 += amount;
};
break;
case 5:
default:
switch (pool) {
case 1 -> pity5Pool1 += amount;
default -> pity5Pool2 += amount;
};
break;
};
}
public void incPityAll() {
pity4++;
pity5++;
pity4Pool1++;
pity4Pool2++;
pity5Pool1++;
pity5Pool2++;
} }
} }

View File

@ -15,6 +15,8 @@ import emu.grasscutter.Grasscutter;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -314,4 +316,68 @@ public final class Utils {
return String.format("%s-%s", locale.getLanguage(), locale.getCountry()); return String.format("%s-%s", locale.getLanguage(), locale.getCountry());
} }
/**
* Performs a linear interpolation using a table of fixed points to create an effective piecewise f(x) = y function.
* @param x
* @param xyArray Array of points in [[x0,y0], ... [xN, yN]] format
* @return f(x) = y
*/
public static int lerp(int x, int[][] xyArray) {
try {
if (x <= xyArray[0][0]){ // Clamp to first point
return xyArray[0][1];
} else if (x >= xyArray[xyArray.length-1][0]) { // Clamp to last point
return xyArray[xyArray.length-1][1];
}
// At this point we're guaranteed to have two lerp points, and pity be somewhere between them.
for (int i=0; i < xyArray.length-1; i++) {
if (x == xyArray[i+1][0]) {
return xyArray[i+1][1];
}
if (x < xyArray[i+1][0]) {
// We are between [i] and [i+1], interpolation time!
// Using floats would be slightly cleaner but we can just as easily use ints if we're careful with order of operations.
int position = x - xyArray[i][0];
int fullDist = xyArray[i+1][0] - xyArray[i][0];
int prevValue = xyArray[i][1];
int fullDelta = xyArray[i+1][1] - prevValue;
return prevValue + ( (position * fullDelta) / fullDist );
}
}
} catch (IndexOutOfBoundsException e) {
Grasscutter.getLogger().error("Malformed lerp point array. Must be of form [[x0, y0], ..., [xN, yN]].");
}
return 0;
}
/**
* Checks if an int is in an int[]
* @param key int to look for
* @param array int[] to look in
* @return key in array
*/
public static boolean intInArray(int key, int[] array) {
for (int i : array) {
if (i == key) {
return true;
}
}
return false;
}
/**
* Return a copy of minuend without any elements found in subtrahend.
* @param minuend The array we want elements from
* @param subtrahend The array whose elements we don't want
* @return The array with only the elements we want, in the order that minuend had them
*/
public static int[] setSubtract(int[] minuend, int[] subtrahend) {
IntList temp = new IntArrayList();
for (int i : minuend) {
if (!intInArray(i, subtrahend)) {
temp.add(i);
}
}
return temp.toIntArray();
}
} }