mirror of
https://github.com/Melledy/Nebula.git
synced 2026-09-19 17:19:58 +02:00
Reimplement gacha (#32)
* Fix some grammar warning * Reimplement gacha * Update GachaResult#toSpinResp declaration --------- Co-authored-by: Yostarcc <lubenwei7758258@gmail.com>
This commit is contained in:
@@ -1,181 +1,253 @@
|
||||
package emu.nebula.game.gacha;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.data.resources.GachaStorageDef;
|
||||
import emu.nebula.game.GameContext;
|
||||
import emu.nebula.game.GameContextModule;
|
||||
import emu.nebula.game.achievement.AchievementCondition;
|
||||
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 emu.nebula.game.gacha.GachaBannerInfo.GachaBannerDraft;
|
||||
import emu.nebula.game.gacha.GachaPityState.GachaPityDraft;
|
||||
import emu.nebula.proto.GachaNewbieInfoOuterClass.GachaNewbieInfo;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GachaModule extends GameContextModule {
|
||||
private final NewbieGachaModule newbieGachaModule = new NewbieGachaModule();
|
||||
|
||||
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
|
||||
public GachaResult spin(Player player, int bannerId, int amount) {
|
||||
var data = GameData.getGachaDataTable().get(bannerId);
|
||||
if (data == null) {
|
||||
if (data == null || !data.isActiveAt(Nebula.getCurrentServerTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var bannerStorage = data.getStorageData();
|
||||
if (bannerStorage == null) {
|
||||
return null;
|
||||
|
||||
var storage = data.getStorageData();
|
||||
|
||||
var manager = player.getGachaManager();
|
||||
synchronized (manager) {
|
||||
var info = manager.getBannerInfo(data);
|
||||
var pityState = manager.getPityState(data.getStorageId());
|
||||
var spinPlan = prepareSpin(player, data, storage, amount, info, pityState);
|
||||
if (spinPlan == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var change = applySpin(player, storage, spinPlan);
|
||||
persistSpin(manager, info, pityState, data, spinPlan);
|
||||
|
||||
player.trigger(AchievementCondition.GachaTotal, amount);
|
||||
player.trigger(AchievementCondition.GachaCharacterTotal, spinPlan.rewardPlan().characterCount());
|
||||
|
||||
return new GachaResult(info, spinPlan.pityDraft(), change, spinPlan.cards());
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
public PlayerChangeInfo recvGuarantee(Player player, int bannerId) {
|
||||
var manager = player.getGachaManager();
|
||||
synchronized (manager) {
|
||||
var info = manager.findBannerInfo(bannerId);
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = GameData.getGachaDataTable().get(bannerId);
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data.isActiveAt(Nebula.getCurrentServerTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data.canGuarantee() || info.getTotal() < data.getGuaranteeTimes()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (info.isUsedGuarantee()) {
|
||||
return null;
|
||||
}
|
||||
info.setUsedGuarantee(true);
|
||||
|
||||
manager.saveBanner(info);
|
||||
return player.getInventory().addItem(data.getGuaranteeTid(), data.getGuaranteeQty());
|
||||
}
|
||||
}
|
||||
|
||||
public List<GachaNewbieInfo> listNewbieInfos(Player player) {
|
||||
return this.newbieGachaModule.listInfos(player);
|
||||
}
|
||||
|
||||
public int[] spinNewbie(Player player, int newbieId) {
|
||||
return this.newbieGachaModule.spin(player, newbieId);
|
||||
}
|
||||
|
||||
public boolean saveNewbie(Player player, int newbieId, Integer index) {
|
||||
return this.newbieGachaModule.save(player, newbieId, index);
|
||||
}
|
||||
|
||||
public PlayerChangeInfo obtainNewbie(Player player, int newbieId, int index) {
|
||||
return this.newbieGachaModule.obtain(player, newbieId, index);
|
||||
}
|
||||
|
||||
private BonusItemsOutcome buildBonusItems(GachaStorageDef storage,
|
||||
int firstTenMultiplier,
|
||||
int amount,
|
||||
GachaBannerDraft snapshot) {
|
||||
var bonusItems = new ItemParamMap();
|
||||
var giveItemsMap = storage.getGiveItemsMap();
|
||||
if (giveItemsMap == null || giveItemsMap.isEmpty()) {
|
||||
return new BonusItemsOutcome(bonusItems, snapshot);
|
||||
}
|
||||
|
||||
int multiplier = 1;
|
||||
var draft = snapshot;
|
||||
if (amount == GachaMode.TEN.getAmount() && !snapshot.usedFirstTen()) {
|
||||
multiplier = Math.max(1, firstTenMultiplier);
|
||||
draft = new GachaBannerDraft(snapshot.total(), true, snapshot.usedGuarantee());
|
||||
}
|
||||
|
||||
for (var entry : giveItemsMap.entries()) {
|
||||
bonusItems.add(entry.getIntKey(), entry.getIntValue() * multiplier * amount);
|
||||
}
|
||||
return new BonusItemsOutcome(bonusItems, draft);
|
||||
}
|
||||
|
||||
private record BonusItemsOutcome(
|
||||
ItemParamMap bonusItems,
|
||||
GachaBannerDraft bannerDraft
|
||||
) {
|
||||
}
|
||||
|
||||
private SpinPlan prepareSpin(Player player,
|
||||
emu.nebula.data.resources.GachaDef data,
|
||||
GachaStorageDef storage,
|
||||
int amount,
|
||||
GachaBannerInfo info,
|
||||
GachaPityState pityState) {
|
||||
var inventory = player.getInventory();
|
||||
int specificConsumeQty = resolveSpecificConsumeQty(inventory, data, amount);
|
||||
int coveredPullCount = specificConsumeQty > 0 && data.getSpecificQty() > 0
|
||||
? specificConsumeQty / data.getSpecificQty()
|
||||
: 0;
|
||||
int remainingDefaultCostReq = storage.getDefaultQty() * Math.max(amount - coveredPullCount, 0);
|
||||
int defaultQty = inventory.getItemCount(storage.getDefaultId());
|
||||
int convertReq = 0;
|
||||
if (remainingDefaultCostReq > defaultQty) {
|
||||
int convertQty = inventory.getResourceCount(storage.getCostId());
|
||||
convertReq = storage.getCostQty() * (remainingDefaultCostReq - defaultQty);
|
||||
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(data);
|
||||
|
||||
// Do gacha
|
||||
var results = new IntArrayList();
|
||||
|
||||
int consumeDefaultQty = Math.min(remainingDefaultCostReq, defaultQty);
|
||||
|
||||
var pityDraft = pityState.copyForSpin();
|
||||
var bannerDraft = info.copyForSpin();
|
||||
var bonusOutcome = buildBonusItems(storage, data.getFirstTenShow(), amount, bannerDraft);
|
||||
bannerDraft = bonusOutcome.bannerDraft();
|
||||
|
||||
var cards = new IntArrayList(amount);
|
||||
for (int i = 0; i < amount; i++) {
|
||||
int id = info.doPull(data);
|
||||
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();
|
||||
|
||||
// Character count (for achievements)
|
||||
int characters = 0;
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Add to count
|
||||
characters += acquire.getCount();
|
||||
} 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());
|
||||
var pullOutcome = GachaRollEngine.pull(data, pityDraft);
|
||||
if (pullOutcome == null || pullOutcome.itemId() <= 0) {
|
||||
Nebula.getLogger().warn("Gacha roll produced invalid item. uid={}, bannerId={}", player.getUid(), data.getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add gold discs
|
||||
bonusItems.add(602, 30 * acquire.getCount());
|
||||
|
||||
pityDraft = pullOutcome.pityDraft();
|
||||
cards.add(pullOutcome.itemId());
|
||||
}
|
||||
|
||||
// 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
|
||||
player.getGachaManager().saveBanner(info);
|
||||
|
||||
// Add history
|
||||
var log = new GachaHistoryLog(data.getStorageId(), data.getId(), results);
|
||||
player.getGachaManager().addGachaHistory(log);
|
||||
|
||||
// Trigger achievements
|
||||
player.trigger(AchievementCondition.GachaTotal, amount);
|
||||
player.trigger(AchievementCondition.GachaCharacterTotal, characters);
|
||||
|
||||
// Complete
|
||||
return new GachaResult(info, change, results);
|
||||
|
||||
bannerDraft = new GachaBannerDraft(
|
||||
bannerDraft.total() + amount,
|
||||
bannerDraft.usedFirstTen(),
|
||||
bannerDraft.usedGuarantee()
|
||||
);
|
||||
|
||||
if (!GachaRewardResolver.isResolvable(cards)) {
|
||||
Nebula.getLogger().warn("Gacha roll contains unresolved rewards. uid={}, bannerId={}", player.getUid(), data.getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
var acquireItems = new ItemAcquireMap(player, cards);
|
||||
var rewardPlan = GachaRewardResolver.resolve(acquireItems, bonusOutcome.bonusItems());
|
||||
if (rewardPlan == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SpinPlan(
|
||||
bannerDraft,
|
||||
pityDraft,
|
||||
cards,
|
||||
rewardPlan,
|
||||
new CostPlan(data.getSpecificTid(), specificConsumeQty, convertReq, consumeDefaultQty)
|
||||
);
|
||||
}
|
||||
|
||||
private PlayerChangeInfo applySpin(Player player, GachaStorageDef storage, SpinPlan spinPlan) {
|
||||
var change = new PlayerChangeInfo();
|
||||
var inventory = player.getInventory();
|
||||
|
||||
if (spinPlan.costPlan().consumeSpecificQty() > 0) {
|
||||
inventory.removeItem(spinPlan.costPlan().specificItemId(), spinPlan.costPlan().consumeSpecificQty(), change);
|
||||
}
|
||||
if (spinPlan.costPlan().convertReq() > 0) {
|
||||
inventory.removeItem(storage.getCostId(), spinPlan.costPlan().convertReq(), change);
|
||||
}
|
||||
if (spinPlan.costPlan().consumeDefaultQty() > 0) {
|
||||
inventory.removeItem(storage.getDefaultId(), spinPlan.costPlan().consumeDefaultQty(), change);
|
||||
}
|
||||
|
||||
GachaRewardResolver.apply(player, spinPlan.rewardPlan(), change);
|
||||
return change;
|
||||
}
|
||||
|
||||
private void persistSpin(GachaManager manager,
|
||||
GachaBannerInfo info,
|
||||
GachaPityState pityState,
|
||||
emu.nebula.data.resources.GachaDef data,
|
||||
SpinPlan spinPlan) {
|
||||
info.overwriteFrom(spinPlan.bannerDraft());
|
||||
pityState.overwriteFrom(spinPlan.pityDraft());
|
||||
manager.saveSpinState(info, data.getStorageId(), data.getId(), spinPlan.cards());
|
||||
}
|
||||
|
||||
private int resolveSpecificConsumeQty(emu.nebula.game.inventory.Inventory inventory,
|
||||
emu.nebula.data.resources.GachaDef data,
|
||||
int amount) {
|
||||
if (data.getSpecificTid() <= 0 || data.getSpecificQty() <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int specificReq = data.getSpecificQty() * amount;
|
||||
int specificQty = inventory.getItemCount(data.getSpecificTid());
|
||||
return Math.min(specificReq, specificQty);
|
||||
}
|
||||
|
||||
private record CostPlan(
|
||||
int specificItemId,
|
||||
int consumeSpecificQty,
|
||||
int convertReq,
|
||||
int consumeDefaultQty
|
||||
) {
|
||||
}
|
||||
|
||||
private record SpinPlan(
|
||||
GachaBannerDraft bannerDraft,
|
||||
GachaPityDraft pityDraft,
|
||||
IntArrayList cards,
|
||||
GachaRewardResolver.RewardPlan rewardPlan,
|
||||
CostPlan costPlan
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user