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,19 +1,7 @@
|
||||
package emu.nebula.data;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import emu.nebula.data.custom.CharGemAttrGroupDef;
|
||||
import emu.nebula.data.resources.*;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -87,8 +75,11 @@ public class GameData {
|
||||
@Getter private static DataTable<DictionaryEntryDef> DictionaryEntryDataTable = new DataTable<>();
|
||||
|
||||
// ===== Gacha =====
|
||||
@Getter private static DataTable<GachaATypeProbDef> GachaATypeProbDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<GachaDef> GachaDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<GachaNewbieDef> GachaNewbieDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<GachaStorageDef> GachaStorageDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<GachaTypeDef> GachaTypeDataTable = new DataTable<>();
|
||||
|
||||
// ===== Story =====
|
||||
@Getter private static DataTable<StoryDef> StoryDataTable = new DataTable<>();
|
||||
@@ -165,7 +156,7 @@ public class GameData {
|
||||
// Activity: Trials
|
||||
@Getter private static DataTable<TrialControlDef> TrialControlDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<TrialGroupDef> TrialGroupDataTable = new DataTable<>();
|
||||
|
||||
|
||||
// Activity: Joint Drill
|
||||
@Getter private static DataTable<JointDrill2LevelDef> JointDrill2LevelDataTable = new DataTable<>();
|
||||
|
||||
@@ -175,7 +166,7 @@ public class GameData {
|
||||
// Activity: Task
|
||||
@Getter private static DataTable<ActivityTaskDef> ActivityTaskDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<ActivityTaskGroupDef> ActivityTaskGroupDataTable = new DataTable<>();
|
||||
|
||||
|
||||
// Activity: Shop
|
||||
@Getter private static DataTable<ActivityShopDef> ActivityShopDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<ActivityShopControlDef> ActivityShopControlDataTable = new DataTable<>();
|
||||
|
||||
@@ -94,11 +94,10 @@ public class ResourceLoader {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Nebula.getLogger().error("Error loading resource file: " + type.name(), e);
|
||||
Nebula.getLogger().error("Error loading resource file: {}", type.name(), e);
|
||||
}
|
||||
|
||||
Nebula.getLogger().info("Loaded " + count + " " + resourceClass.getSimpleName() + "s.");
|
||||
|
||||
Nebula.getLogger().info("Loaded {} {}s.", count, resourceClass.getSimpleName());
|
||||
}
|
||||
|
||||
// Utility
|
||||
@@ -132,7 +131,7 @@ public class ResourceLoader {
|
||||
field.setAccessible(true);
|
||||
table = (DataTable<T>) field.get(null);
|
||||
} catch (Exception e) {
|
||||
|
||||
// ignore exception
|
||||
} finally {
|
||||
field.setAccessible(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package emu.nebula.data.resources;
|
||||
|
||||
import emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.ResourceType;
|
||||
import emu.nebula.data.ResourceType.LoadPriority;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntCollection;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@ResourceType(name = "GachaATypeProb.json", loadPriority = LoadPriority.HIGH)
|
||||
public class GachaATypeProbDef extends BaseDef {
|
||||
private int Group;
|
||||
private int Times;
|
||||
private int Prob;
|
||||
|
||||
private static final Int2ObjectMap<Int2IntMap> probByGroupAndTimes = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
// Compose a key from (Group, Times): high 16 bits = Group, low 16 bits = Times.
|
||||
return (Group << 16) | (Times & 0xFFFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
// Indexed as group (miss times -> probability) for O(1) runtime lookup
|
||||
var groupMap = probByGroupAndTimes.computeIfAbsent(this.Group, i -> new Int2IntOpenHashMap());
|
||||
groupMap.put(this.Times, this.Prob);
|
||||
}
|
||||
|
||||
public static int getProb(int group, int times, int fallback) {
|
||||
var groupMap = probByGroupAndTimes.get(group);
|
||||
if (groupMap == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (groupMap.containsKey(times)) {
|
||||
return groupMap.get(times);
|
||||
}
|
||||
|
||||
// Times=0 is treated as the group's default curve point.
|
||||
return groupMap.getOrDefault(0, fallback);
|
||||
}
|
||||
|
||||
public static int getMaxProb() {
|
||||
int max = 0;
|
||||
// Used by the roll engine to scale random range safely when config max exceeds 10000.
|
||||
for (var groupMap : probByGroupAndTimes.values()) {
|
||||
IntCollection values = groupMap.values();
|
||||
for (var valueIterator = values.iterator(); valueIterator.hasNext();) {
|
||||
int value = valueIterator.nextInt();
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package emu.nebula.data.resources;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.data.ResourceType;
|
||||
import emu.nebula.data.ResourceType.LoadPriority;
|
||||
import emu.nebula.util.Utils;
|
||||
import emu.nebula.util.WeightedList;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@ResourceType(name = "Gacha.json")
|
||||
@ResourceType(name = "Gacha.json", loadPriority = LoadPriority.LOWEST)
|
||||
public class GachaDef extends BaseDef {
|
||||
private int Id;
|
||||
private int StorageId;
|
||||
@@ -16,6 +21,14 @@ public class GachaDef extends BaseDef {
|
||||
private int GuaranteeTimes;
|
||||
private int GuaranteeTid;
|
||||
private int GuaranteeQty;
|
||||
private int ATypeGuaranteeTimes;
|
||||
|
||||
private int SpecificTid;
|
||||
private int SpecificQty;
|
||||
|
||||
private int FirstTenShow;
|
||||
private String StartTime;
|
||||
private String EndTime;
|
||||
|
||||
// Packages
|
||||
private int ATypePkg;
|
||||
@@ -31,6 +44,11 @@ public class GachaDef extends BaseDef {
|
||||
private transient WeightedList<GachaPackage> packageA;
|
||||
private transient WeightedList<GachaPackage> packageB;
|
||||
private transient WeightedList<GachaPackage> packageC;
|
||||
private transient GachaTypeDef typeData;
|
||||
private transient IntList allowedCoinItems;
|
||||
private transient long startTimeSeconds;
|
||||
private transient long endTimeSeconds;
|
||||
private transient boolean valid = true;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
@@ -44,18 +62,110 @@ public class GachaDef extends BaseDef {
|
||||
public GachaStorageDef getStorageData() {
|
||||
return GameData.getGachaStorageDataTable().get(this.getStorageId());
|
||||
}
|
||||
|
||||
|
||||
public GachaTypeDef getTypeData() {
|
||||
if (this.typeData != null) {
|
||||
return this.typeData;
|
||||
}
|
||||
|
||||
return GameData.getGachaTypeDataTable().get(this.getGachaType());
|
||||
}
|
||||
|
||||
public boolean containsAllowedCoinItem(int itemId) {
|
||||
return this.allowedCoinItems != null && this.allowedCoinItems.contains(itemId);
|
||||
}
|
||||
|
||||
public boolean isActiveAt(long now) {
|
||||
return this.valid && now >= this.startTimeSeconds && now <= this.endTimeSeconds;
|
||||
}
|
||||
|
||||
public int getDisplayAUpGuaranteeTimes() {
|
||||
if (this.ATypeGuaranteeTimes > 0) {
|
||||
return this.ATypeGuaranteeTimes;
|
||||
}
|
||||
|
||||
var storage = this.getStorageData();
|
||||
return storage != null ? storage.getAUpGuaranteeTimes() : 0;
|
||||
}
|
||||
|
||||
private boolean hasValidPackage(int packageId) {
|
||||
if (packageId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var pkg = GachaPkgDef.getPackageById(packageId);
|
||||
return pkg != null && pkg.size() > 0;
|
||||
}
|
||||
|
||||
private boolean isSpinConfigValid() {
|
||||
boolean hasValidA = (this.ATypePkg > 0 && hasValidPackage(this.ATypePkg))
|
||||
|| (this.ATypeUpPkg > 0 && hasValidPackage(this.ATypeUpPkg));
|
||||
boolean hasValidB = (this.BTypePkg > 0 && hasValidPackage(this.BTypePkg))
|
||||
|| (this.BGuaranteePkg > 0 && hasValidPackage(this.BGuaranteePkg))
|
||||
|| (this.BTypeUpPkg > 0 && hasValidPackage(this.BTypeUpPkg));
|
||||
boolean hasValidC = this.CTypePkg > 0 && hasValidPackage(this.CTypePkg);
|
||||
|
||||
return this.packageA != null && this.packageA.size() > 0
|
||||
&& this.packageB != null && this.packageB.size() > 0
|
||||
&& this.packageC != null && this.packageC.size() > 0
|
||||
&& hasValidA && hasValidB && hasValidC;
|
||||
}
|
||||
|
||||
private void markInvalid(String message) {
|
||||
this.valid = false;
|
||||
this.startTimeSeconds = 1L;
|
||||
this.endTimeSeconds = 0L;
|
||||
Nebula.getLogger().error("Skip invalid gacha config for banner {}: {}", this.getId(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
this.valid = true;
|
||||
this.startTimeSeconds = 0L;
|
||||
this.endTimeSeconds = Long.MAX_VALUE;
|
||||
|
||||
this.packageA = new WeightedList<>();
|
||||
this.packageB = new WeightedList<>();
|
||||
this.packageC = new WeightedList<>();
|
||||
this.allowedCoinItems = new IntArrayList();
|
||||
|
||||
this.typeData = this.getTypeData();
|
||||
if (this.typeData == null) {
|
||||
markInvalid("invalid GachaType " + this.getGachaType());
|
||||
return;
|
||||
}
|
||||
if (this.typeData.getCoinItem() != null) {
|
||||
this.allowedCoinItems.addAll(this.typeData.getCoinItem());
|
||||
}
|
||||
|
||||
if (this.StartTime != null && !this.StartTime.isBlank()) {
|
||||
this.startTimeSeconds = Utils.dateToMilliseconds(this.StartTime) / 1000;
|
||||
}
|
||||
if (this.EndTime != null && !this.EndTime.isBlank()) {
|
||||
this.endTimeSeconds = Utils.dateToMilliseconds(this.EndTime) / 1000;
|
||||
}
|
||||
|
||||
// Get storage
|
||||
var storage = this.getStorageData();
|
||||
|
||||
if (storage == null) {
|
||||
markInvalid("invalid StorageId " + this.getStorageId());
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isNewbieBanner = GameData.getGachaNewbieDataTable().containsKey(this.getId());
|
||||
if (!isNewbieBanner) {
|
||||
// DefaultId can be a dedicated ticket item that is not listed in CoinItem.
|
||||
// Only the fallback currency (CostId) is required to be in the type whitelist.
|
||||
if (!this.containsAllowedCoinItem(storage.getCostId())) {
|
||||
markInvalid("CostId not allowed by GachaType: " + storage.getCostId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Package A
|
||||
this.packageA = new WeightedList<GachaPackage>();
|
||||
|
||||
if (this.ATypePkg > 0) {
|
||||
packageA.add(
|
||||
10000 - storage.getATypeUpProb(),
|
||||
10000 - storage.getATypeUpProb(),
|
||||
new GachaPackage(GachaPackageType.A, this.ATypePkg)
|
||||
);
|
||||
}
|
||||
@@ -65,38 +175,38 @@ public class GachaDef extends BaseDef {
|
||||
new GachaPackage(GachaPackageType.A_UP, this.ATypeUpPkg)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Package B
|
||||
this.packageB = new WeightedList<GachaPackage>();
|
||||
|
||||
if (this.BTypePkg > 0) {
|
||||
packageB.add(
|
||||
storage.getBTypeProb(),
|
||||
storage.getBTypeProb(),
|
||||
new GachaPackage(GachaPackageType.B, this.BTypePkg)
|
||||
);
|
||||
}
|
||||
if (this.BGuaranteePkg > 0) {
|
||||
packageB.add(
|
||||
storage.getBTypeGuaranteeProb(),
|
||||
storage.getBTypeGuaranteeProb(),
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isSpinConfigValid()) {
|
||||
markInvalid("spin package/probability composition is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package emu.nebula.data.resources;
|
||||
|
||||
import emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.ResourceType;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@ResourceType(name = "GachaNewbie.json")
|
||||
public class GachaNewbieDef extends BaseDef {
|
||||
private int Id;
|
||||
private int SpinCount;
|
||||
private int SaveCount;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return this.Id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package emu.nebula.data.resources;
|
||||
import emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.ResourceType;
|
||||
import emu.nebula.data.ResourceType.LoadPriority;
|
||||
import emu.nebula.game.inventory.ItemParamMap;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@@ -15,13 +16,29 @@ public class GachaStorageDef extends BaseDef {
|
||||
private int CostId;
|
||||
private int CostQty;
|
||||
|
||||
private int ATypeGroup;
|
||||
private int AUpGuaranteeTimes;
|
||||
private int ATypeUpProb;
|
||||
private int ATypeUpShowProb;
|
||||
|
||||
private int BTypeProb;
|
||||
private int BGuaranteeTimes;
|
||||
private int BTypeUpProb;
|
||||
private int BTypeUpShowProb;
|
||||
private int BTypeGuaranteeProb;
|
||||
|
||||
private String GiveItems;
|
||||
private transient ItemParamMap giveItemsMap = new ItemParamMap();
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return Id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
if (this.GiveItems != null && !this.GiveItems.isEmpty()) {
|
||||
this.giveItemsMap = ItemParamMap.fromJsonString(this.GiveItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package emu.nebula.data.resources;
|
||||
|
||||
import emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.ResourceType;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@ResourceType(name = "GachaType.json")
|
||||
public class GachaTypeDef extends BaseDef {
|
||||
private int Id;
|
||||
private IntArrayList CoinItem;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return this.Id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
if (this.CoinItem == null) {
|
||||
this.CoinItem = new IntArrayList();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsCoinItem(int itemId) {
|
||||
return this.CoinItem != null && this.CoinItem.contains(itemId);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user