Implement gacha banners (not newbie)

This commit is contained in:
Melledy
2025-11-01 04:17:44 -07:00
parent e9f991355a
commit 37b74c9b35
20 changed files with 697 additions and 40 deletions

View File

@@ -50,6 +50,9 @@ public class GameData {
@Getter private static DataTable<CharGemInstanceDef> CharGemInstanceDataTable = new DataTable<>();
@Getter private static DataTable<WeekBossLevelDef> WeekBossLevelDataTable = new DataTable<>();
@Getter private static DataTable<GachaDef> GachaDataTable = new DataTable<>();
@Getter private static DataTable<GachaStorageDef> GachaStorageDataTable = new DataTable<>();
@Getter private static DataTable<WorldClassDef> WorldClassDataTable = new DataTable<>();
@Getter private static DataTable<GuideGroupDef> GuideGroupDataTable = new DataTable<>();
@Getter private static DataTable<HandbookDef> HandbookDataTable = new DataTable<>();

View File

@@ -0,0 +1,113 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.util.WeightedList;
import lombok.Getter;
@Getter
@ResourceType(name = "Gacha.json")
public class GachaDef extends BaseDef {
private int Id;
private int StorageId;
private int GachaType;
private int GuaranteeTimes;
private int GuaranteeTid;
private int GuaranteeQty;
// Packages
private int ATypePkg;
private int BTypePkg;
private int CTypePkg;
private int ATypeUpPkg;
private int BTypeUpPkg;
private int CTypeUpPkg;
private int BGuaranteePkg;
private transient WeightedList<GachaPackage> packageA;
private transient WeightedList<GachaPackage> packageB;
private transient WeightedList<GachaPackage> packageC;
@Override
public int getId() {
return Id;
}
public GachaStorageDef getStorageData() {
return GameData.getGachaStorageDataTable().get(this.getStorageId());
}
@Override
public void onLoad() {
// Get storage
var storage = this.getStorageData();
// Package A
this.packageA = new WeightedList<GachaPackage>();
if (this.ATypePkg > 0) {
packageA.add(
10000 - storage.getATypeUpProb(),
new GachaPackage(GachaPackageType.A, this.ATypePkg)
);
} if (this.ATypeUpPkg > 0) {
packageA.add(
storage.getATypeUpProb(),
new GachaPackage(GachaPackageType.A_UP, this.ATypeUpPkg)
);
}
// Package B
this.packageB = new WeightedList<GachaPackage>();
if (this.BTypePkg > 0) {
packageB.add(
10000 - storage.getBTypeUpProb(),
new GachaPackage(GachaPackageType.B, this.BTypePkg)
);
} else if (this.BGuaranteePkg > 0) {
packageB.add(
10000 - storage.getBTypeUpProb(),
new GachaPackage(GachaPackageType.B, this.BGuaranteePkg)
);
} if (this.BTypeUpPkg > 0) {
packageB.add(
storage.getBTypeUpProb(),
new GachaPackage(GachaPackageType.B_UP, this.BTypeUpPkg)
);
}
// Package C
this.packageC = new WeightedList<GachaPackage>();
if (this.CTypePkg > 0) {
packageC.add(
10000,
new GachaPackage(GachaPackageType.C, this.CTypePkg)
);
}
}
@Getter
public static class GachaPackage {
private GachaPackageType type;
private int id;
public GachaPackage(GachaPackageType type, int id) {
this.type = type;
this.id = id;
}
}
public enum GachaPackageType {
A,
A_UP,
B,
B_UP,
C;
}
}

View File

@@ -0,0 +1,35 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import emu.nebula.util.WeightedList;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
@ResourceType(name = "GachaPkg.json", loadPriority = LoadPriority.LOW)
public class GachaPkgDef extends BaseDef {
private int PkgId;
private int GoodsId;
private int Weight;
private static final Int2ObjectMap<WeightedList<Integer>> packages = new Int2ObjectOpenHashMap<>();
@Override @Deprecated
public int getId() {
return PkgId;
}
public static WeightedList<Integer> getPackageById(int packageId) {
return packages.get(packageId);
}
@Override
public void onLoad() {
// Add to package
var list = packages.computeIfAbsent(this.getPkgId(), i -> new WeightedList<Integer>());
list.add(this.getWeight(), this.getGoodsId());
}
}

View File

@@ -0,0 +1,26 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = "GachaStorage.json", loadPriority = LoadPriority.HIGH)
public class GachaStorageDef extends BaseDef {
private int Id;
private int DefaultId;
private int DefaultQty;
private int CostId;
private int CostQty;
private int ATypeUpProb;
private int BTypeUpProb;
private int BTypeGuaranteeProb;
@Override
public int getId() {
return Id;
}
}

View File

@@ -4,6 +4,7 @@ import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import emu.nebula.game.gacha.GachaModule;
import emu.nebula.game.player.PlayerModule;
import emu.nebula.net.GameSession;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
@@ -16,13 +17,16 @@ public class GameContext {
// Modules
private final PlayerModule playerModule;
private final GachaModule gachaModule;
// Cleanup thread
private final Timer cleanupTimer;
public GameContext() {
this.sessions = new Object2ObjectOpenHashMap<>();
this.playerModule = new PlayerModule(this);
this.gachaModule = new GachaModule(this);
this.cleanupTimer = new Timer();
this.cleanupTimer.scheduleAtFixedRate(new CleanupTask(this), 0, TimeUnit.SECONDS.toMillis(60));

View File

@@ -141,7 +141,7 @@ public class Character implements GameDatabaseObject {
int exp = 0;
// Check if item is an exp item
for (var entry : params.getEntrySet()) {
for (var entry : params.entries()) {
var data = GameData.getCharItemExpDataTable().get(entry.getIntKey());
if (data == null) return null;

View File

@@ -130,7 +130,7 @@ public class GameDisc implements GameDatabaseObject {
int exp = 0;
// Check if item is an exp item
for (var entry : params.getEntrySet()) {
for (var entry : params.entries()) {
var data = GameData.getDiscItemExpDataTable().get(entry.getIntKey());
if (data == null) return null;

View File

@@ -0,0 +1,99 @@
package emu.nebula.game.gacha;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.data.resources.GachaDef;
import emu.nebula.data.resources.GachaDef.GachaPackage;
import emu.nebula.data.resources.GachaPkgDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
@Entity(value = "banner_info", useDiscriminator = false)
public class GachaBannerInfo implements GameDatabaseObject {
@Id
private ObjectId id;
private int bannerId;
private int playerUid;
private int total;
private int missTimesA;
private int missTimesUpA;
private int missTimesB;
private boolean usedGuarantee;
@Deprecated //Morphia only
public GachaBannerInfo() {
}
public GachaBannerInfo(Player player, GachaDef data) {
this.playerUid = player.getUid();
this.bannerId = data.getId();
}
public int doPull(GachaDef data) {
// Pull chances
int chanceA = 20; // 2%
int chanceB = 100; // 8%
// 4 star pity
if (this.missTimesB >= 9) {
chanceB = 1000;
}
// 5 star pity
if (this.missTimesA >= 159) {
chanceA = 1000;
chanceB = 0;
}
// Add miss times
this.missTimesB++;
this.missTimesA++;
//this.missTimesUpA++;
// Get random
int random = Utils.randomRange(1, 1000);
GachaPackage gp = null;
if (random <= chanceA) {
// Reset pity
this.missTimesA = 0;
// Get A package
gp = data.getPackageA().next();
} else if (random <= chanceB) {
// Add miss times
this.missTimesB = 0;
// Get B package
gp = data.getPackageB().next();
} else {
// Get C package
gp = data.getPackageC().next();
}
// Sanity check
if (gp == null) {
return 0;
}
// Get package
var pkg = GachaPkgDef.getPackageById(gp.getId());
if (pkg == null) {
return 0;
}
// Add total pulls
this.total++;
// Get random id
return pkg.next();
}
}

View File

@@ -0,0 +1,46 @@
package emu.nebula.game.gacha;
import java.util.Collection;
import emu.nebula.Nebula;
import emu.nebula.data.resources.GachaDef;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public class GachaManager extends PlayerManager {
private final Int2ObjectMap<GachaBannerInfo> bannerInfos;
private boolean loaded;
public GachaManager(Player player) {
super(player);
this.bannerInfos = new Int2ObjectOpenHashMap<>();
}
public synchronized Collection<GachaBannerInfo> getBannerInfos() {
return this.bannerInfos.values();
}
public synchronized GachaBannerInfo getBannerInfo(GachaDef gachaData) {
if (!this.loaded) {
this.loadFromDatabase();
}
return this.bannerInfos.computeIfAbsent(
gachaData.getId(),
i -> new GachaBannerInfo(this.getPlayer(), gachaData)
);
}
private void loadFromDatabase() {
var db = Nebula.getGameDatabase();
db.getObjects(GachaBannerInfo.class, "playerUid", getPlayerUid()).forEach(bannerInfo -> {
this.bannerInfos.put(bannerInfo.getBannerId(), bannerInfo);
});
this.loaded = true;
}
}

View File

@@ -0,0 +1,166 @@
package emu.nebula.game.gacha;
import emu.nebula.data.GameData;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import emu.nebula.game.inventory.ItemAcquireMap;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.inventory.ItemType;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.Public.Transform;
import it.unimi.dsi.fastutil.ints.IntArrayList;
public class GachaModule extends GameContextModule {
public GachaModule(GameContext context) {
super(context);
}
public GachaResult spin(Player player, int bannerId, int mode) {
// Get pull count
int amount = mode == 2 ? 10 : 1;
// Get banner data
var banner = GameData.getGachaDataTable().get(bannerId);
if (banner == null) {
return null;
}
var bannerStorage = banner.getStorageData();
if (bannerStorage == null) {
return null;
}
// Create change info
var change = new PlayerChangeInfo();
// Check if we have the materials to gacha TODO
int costQty = player.getInventory().getItemCount(bannerStorage.getDefaultId());
int costReq = bannerStorage.getDefaultQty() * amount;
if (costReq > costQty) {
// Not enough materials, check if we can convert
int convertQty = player.getInventory().getResourceCount(bannerStorage.getCostId());
int convertReq = bannerStorage.getCostQty() * (costReq - costQty);
// Check if we can buy pulls
if (convertReq > convertQty) {
return null;
}
// Convert to pull currency
player.getInventory().removeItem(bannerStorage.getCostId(), convertReq, change);
}
// Consume pull currency
player.getInventory().removeItem(bannerStorage.getDefaultId(), Math.min(costReq, costQty), change);
// Get gacha banner info
var info = player.getGachaManager().getBannerInfo(banner);
// Do gacha
var results = new IntArrayList();
for (int i = 0; i < amount; i++) {
int id = info.doPull(banner);
if (id <= 0) continue;
results.add(id);
}
// Setup variables
var acquireItems = new ItemAcquireMap(player, results);
var transformItemsSrc = new ItemParamMap();
var transformItemsDst = new ItemParamMap();
var bonusItems = new ItemParamMap();
// Add for player
for (var entry : acquireItems.getItems().int2ObjectEntrySet()) {
// Get ids and aquire params
int id = entry.getIntKey();
var acquire = entry.getValue();
// Add to player
if (acquire.getType() == ItemType.Char) {
// Get add amount
int count = acquire.getCount();
// Add char to player
if (acquire.getBegin() == 0) {
player.getInventory().addItem(id, 1, change);
count--;
}
// Talent material
if (count > 0) {
var characterData = GameData.getCharacterDataTable().get(id);
if (characterData == null) continue;
transformItemsSrc.add(id, count);
transformItemsDst.add(characterData.getFragmentsId(), characterData.getTransformQty() * count);
transformItemsDst.add(24, 40 * count); // Expert permits
}
} else if (acquire.getType() == ItemType.Disc) {
// Get add amount
int begin = acquire.getBegin();
int count = acquire.getCount();
// Add disc to player
if (begin == 0) {
player.getInventory().addItem(id, 1, change);
count--;
begin++;
}
// Talent material
int maxTransformCount = Math.max(6 - begin, 0);
int transformCount = Math.min(count, maxTransformCount);
int extraCount = count - maxTransformCount;
// Transform
if (transformCount > 0) {
var discData = GameData.getDiscDataTable().get(id);
if (discData == null) continue;
// Star material
transformItemsSrc.add(id, transformCount);
transformItemsDst.add(discData.getTransformItemId(), transformCount);
} else if (extraCount > 0) {
// Permit
transformItemsSrc.add(id, extraCount);
transformItemsDst.add(23, 100 * extraCount);
}
// Add Travel permits
bonusItems.add(23, 100 * acquire.getCount());
} else {
// Should never happen
bonusItems.add(id, acquire.getCount());
}
// Add gold discs
bonusItems.add(602, 30 * acquire.getCount());
}
// Add transform items to extra items
bonusItems.add(transformItemsDst); // Add transform items
// Add extra items
player.getInventory().addItems(bonusItems, change);
// Add acquire/transform protos
change.add(acquireItems.toProto());
var transform = Transform.newInstance();
transformItemsSrc.toItemTemplateStream().forEach(transform::addSrc);
transformItemsDst.toItemTemplateStream().forEach(transform::addDst);
change.add(transform);
// Save banner info to database
info.save();
// Complete
return new GachaResult(info, change, results);
}
}

View File

@@ -0,0 +1,19 @@
package emu.nebula.game.gacha;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
public class GachaResult {
private GachaBannerInfo info;
private PlayerChangeInfo change;
private IntList cards;
public GachaResult(GachaBannerInfo info, PlayerChangeInfo change, IntList cards) {
this.info = info;
this.change = change;
this.cards = cards;
}
}

View File

@@ -207,7 +207,7 @@ public class Inventory extends PlayerManager {
}
// Add items
for (var param : params.getEntrySet()) {
for (var param : params.entries()) {
this.addItem(param.getIntKey(), param.getIntValue(), changes);
}
@@ -242,7 +242,7 @@ public class Inventory extends PlayerManager {
}
// Remove items
for (var param : params.getEntrySet()) {
for (var param : params.entries()) {
this.removeItem(param.getIntKey(), param.getIntValue(), changes);
}
@@ -293,7 +293,7 @@ public class Inventory extends PlayerManager {
public synchronized boolean verifyItems(ItemParamMap params) {
boolean hasItems = true;
for (var param : params.getEntrySet()) {
for (var param : params.entries()) {
hasItems = this.verifyItem(param.getIntKey(), param.getIntValue());
if (!hasItems) {

View File

@@ -0,0 +1,92 @@
package emu.nebula.game.inventory;
import emu.nebula.data.GameData;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.AcqInfo;
import emu.nebula.proto.Public.Acquire;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
public class ItemAcquireMap {
private final Int2ObjectMap<ItemAcquireParam> items;
public ItemAcquireMap(Player player, IntList list) {
this.items = new Int2ObjectOpenHashMap<>();
for (int id : list) {
// Get item data
var data = GameData.getItemDataTable().get(id);
if (data == null) {
continue;
}
// Add to acquire map
if (!this.getItems().containsKey(id)) {
// Get starting count
int count = 0;
// Check item type
if (data.getItemType() == ItemType.Char) {
var character = player.getCharacters().getCharacterById(id);
if (character != null) {
count = 1;
}
} else if (data.getItemType() == ItemType.Disc) {
var disc = player.getCharacters().getDiscById(id);
if (disc != null) {
count = 1;
count += disc.getStar();
count += player.getInventory().getItemCount(disc.getData().getTransformItemId());
}
}
var acquireInfo = new ItemAcquireParam(data.getItemType(), count);
acquireInfo.add();
this.getItems().put(id, acquireInfo);
} else {
this.getItems().get(id).add();
}
}
}
// Proto
public Acquire toProto() {
var proto = Acquire.newInstance();
for (var entry : this.items.int2ObjectEntrySet()) {
var a = AcqInfo.newInstance()
.setTid(entry.getIntKey())
.setBegin(entry.getValue().getBegin())
.setCount(entry.getValue().getCount());
proto.addList(a);
}
return proto;
}
@Getter
public static class ItemAcquireParam {
private ItemType type;
private int begin;
private int count;
public ItemAcquireParam(ItemType type, int i) {
this.type = type;
this.begin = i;
}
public void add() {
this.count++;
}
}
}

View File

@@ -7,16 +7,14 @@ import java.util.stream.Stream;
import emu.nebula.proto.Public.Item;
import emu.nebula.proto.Public.ItemInfo;
import emu.nebula.proto.Public.ItemTpl;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2IntLinkedOpenHashMap;
import us.hebi.quickbuf.RepeatedMessage;
public class ItemParamMap extends Int2IntOpenHashMap {
public class ItemParamMap extends Int2IntLinkedOpenHashMap {
private static final long serialVersionUID = -4186524272780523459L;
public FastEntrySet entries() {
return this.int2IntEntrySet();
}
@Override @Deprecated
public int addTo(int itemId, int count) {
return this.add(itemId, count);
@@ -55,9 +53,7 @@ public class ItemParamMap extends Int2IntOpenHashMap {
return params;
}
//
public FastEntrySet getEntrySet() {
public FastEntrySet entries() {
return this.int2IntEntrySet();
}
@@ -74,13 +70,13 @@ public class ItemParamMap extends Int2IntOpenHashMap {
}
public Stream<ItemTpl> toItemTemplateStream() {
return getEntrySet()
return entries()
.stream()
.map(e -> ItemTpl.newInstance().setTid(e.getIntKey()).setQty(e.getIntValue()));
}
public Stream<Item> toItemProtoStream() {
return getEntrySet()
return entries()
.stream()
.map(e -> Item.newInstance().setTid(e.getIntKey()).setQty(e.getIntValue()));
}

View File

@@ -14,6 +14,7 @@ import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.account.Account;
import emu.nebula.game.character.CharacterStorage;
import emu.nebula.game.formation.FormationManager;
import emu.nebula.game.gacha.GachaManager;
import emu.nebula.game.instance.InstanceManager;
import emu.nebula.game.inventory.Inventory;
import emu.nebula.game.mail.Mailbox;
@@ -66,6 +67,7 @@ public class Player implements GameDatabaseObject {
// Managers
private final transient CharacterStorage characters;
private final transient Inventory inventory;
private transient GachaManager gachaManager;
// Referenced data
private transient FormationManager formations;
@@ -79,6 +81,7 @@ public class Player implements GameDatabaseObject {
this.sessions = new HashSet<>();
this.characters = new CharacterStorage(this);
this.inventory = new Inventory(this);
this.gachaManager = new GachaManager(this);
}
public Player(Account account, String name, boolean gender) {

View File

@@ -38,7 +38,6 @@ public class NetMsgIdUtils {
return msgIdMap.getOrDefault(msgId, "UNKNOWN");
}
@SuppressWarnings("unused")
public static void dumpPacketIds() {
try (FileWriter writer = new FileWriter("./MsgIds_" + GameConstants.VERSION + ".json")) {
// Create sorted tree map

View File

@@ -2,6 +2,7 @@ package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.GachaInformation.GachaInfo;
import emu.nebula.proto.GachaInformation.GachaInformationResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@@ -11,10 +12,23 @@ public class HandlerGachaInformationReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Build response
var rsp = GachaInformationResp.newInstance();
// TODO
for (var bannerInfo : session.getPlayer().getGachaManager().getBannerInfos()) {
var info = GachaInfo.newInstance()
.setId(bannerInfo.getBannerId())
.setGachaTotalTimes(bannerInfo.getTotal())
.setTotalTimes(bannerInfo.getTotal())
.setAupMissTimes(bannerInfo.getMissTimesA())
.setAMissTimes(bannerInfo.getMissTimesA())
.setReveFirstTenReward(true)
.setRecvGuaranteeReward(bannerInfo.isUsedGuarantee());
rsp.addInformation(info);
}
// Encode and send
return session.encodeMsg(NetMsgId.gacha_information_succeed_ack, rsp);
}

View File

@@ -2,7 +2,8 @@ package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.GachaInformation.GachaInformationResp;
import emu.nebula.proto.GachaNewbieInfoOuterClass.GachaNewbieInfo;
import emu.nebula.proto.GachaNewbieInfoOuterClass.GachaNewbieInfoResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@@ -11,7 +12,12 @@ public class HandlerGachaNewbieInfoReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = GachaInformationResp.newInstance();
var rsp = GachaNewbieInfoResp.newInstance();
var info = GachaNewbieInfo.newInstance()
.setId(5)
.setReceive(true);
rsp.addList(info);
return session.encodeMsg(NetMsgId.gacha_newbie_info_succeed_ack, rsp);
}

View File

@@ -2,50 +2,51 @@ package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.Nebula;
import emu.nebula.net.GameSession;
import emu.nebula.proto.GachaSpin.GachaCard;
import emu.nebula.proto.GachaSpin.GachaSpinReq;
import emu.nebula.proto.GachaSpin.GachaSpinResp;
import emu.nebula.proto.Public.ItemTpl;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import emu.nebula.net.HandlerId;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.gacha_spin_req)
public class HandlerGachaSpinReq extends NetHandler {
@SuppressWarnings("unused")
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = GachaSpinReq.parseFrom(message);
// Temp
var list = new IntArrayList();
// Do gacha
var result = Nebula.getGameContext().getGachaModule().spin(
session.getPlayer(),
req.getId(),
req.getMode()
);
for (var def : GameData.getCharacterDataTable()) {
if (def.getGrade() == 1 && def.isAvailable()) {
list.add(def.getId());
}
if (result == null) {
return session.encodeMsg(NetMsgId.gacha_spin_failed_ack);
}
// Build response
var rsp = GachaSpinResp.newInstance()
.setTime(Nebula.getCurrentTime());
rsp.getMutableChange();
rsp.getMutableNextPackage();
for (int i = 0; i < 10; i++) {
int id = Utils.randomElement(list);
.setTime(Nebula.getCurrentTime())
.setAMissTimes(result.getInfo().getMissTimesA())
.setAupMissTimes(result.getInfo().getMissTimesA())
.setTotalTimes(result.getInfo().getTotal())
.setGachaTotalTimes(result.getInfo().getTotal())
.setAupGuaranteeTimes(result.getInfo().isUsedGuarantee() ? 0 : 1)
.setChange(result.getChange().toProto());
for (int id : result.getCards()) {
var card = GachaCard.newInstance()
.setCard(ItemTpl.newInstance().setTid(id).setQty(1));
rsp.addCards(card);
}
// Encode and send response
return session.encodeMsg(NetMsgId.gacha_spin_succeed_ack, rsp);
}

View File

@@ -0,0 +1,35 @@
package emu.nebula.util;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.concurrent.ThreadLocalRandom;
public class WeightedList<E> {
private final NavigableMap<Double, E> map = new TreeMap<>();
private double total = 0;
public WeightedList() {
}
public WeightedList<E> add(double weight, E result) {
if (weight <= 0) return this;
total += weight;
map.put(total, result);
return this;
}
public E next() {
double value = ThreadLocalRandom.current().nextDouble() * total;
return map.higherEntry(value).getValue();
}
public int size() {
return map.size();
}
public void clear() {
this.map.clear();
this.total = 0;
}
}