Fix mall, shop, mall/shop refresh logic and battle pass flows

This commit is contained in:
Yostarcc
2026-05-25 18:43:02 -07:00
committed by Melledy
parent c1c09c6594
commit 57c05c8e32
45 changed files with 2171 additions and 232 deletions
+2 -1
View File
@@ -57,6 +57,7 @@ public class GameData {
@Getter private static DataTable<MallPackageDef> MallPackageDataTable = new DataTable<>();
@Getter private static DataTable<MallShopDef> MallShopDataTable = new DataTable<>();
@Getter private static DataTable<MallGemDef> MallGemDataTable = new DataTable<>();
@Getter private static DataTable<MonthlyCardRewardDef> MonthlyCardRewardDataTable = new DataTable<>();
@Getter private static DataTable<ResidentShopDef> ResidentShopDataTable = new DataTable<>();
@Getter private static DataTable<ResidentGoodsDef> ResidentGoodsDataTable = new DataTable<>();
@@ -171,4 +172,4 @@ public class GameData {
@Getter private static DataTable<ActivityShopDef> ActivityShopDataTable = new DataTable<>();
@Getter private static DataTable<ActivityShopControlDef> ActivityShopControlDataTable = new DataTable<>();
@Getter private static DataTable<ActivityGoodsDef> ActivityGoodsDataTable = new DataTable<>();
}
}
@@ -4,13 +4,65 @@ import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Getter
@ResourceType(name = "BattlePass.json")
public class BattlePassDef extends BaseDef {
private int ID;
private String Name;
private String StartTime;
private String EndTime;
private String LuxuryProductId;
private int LuxuryPrice;
private String LuxuryShowPrice;
private int LuxuryBonusLevel;
private int LuxuryTid;
private int LuxuryQty;
private String PremiumProductId;
private int PremiumPrice;
private String PremiumShowPrice;
private String ComplementaryProductId;
private int ComplementaryPrice;
private String ComplementaryShowPrice;
private int ComplementaryTid;
private int ComplementaryQty;
private String OriginShowPrice;
private String CoverColor;
private int Cover;
private List<Integer> PremiumShowItems;
private List<Integer> LuxuryShowItems;
private int OutfitPackageShowItem;
private transient long endTimeTimestamp;
private transient long startTimeTimestamp;
@Override
public int getId() {
return ID;
}
@Override
public void onLoad() {
// Parse start time to timestamp
try {
var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
var zonedDateTime = ZonedDateTime.parse(StartTime, formatter);
this.startTimeTimestamp = zonedDateTime.toInstant().toEpochMilli() / 1000;
} catch (Exception e) {
this.startTimeTimestamp = 0;
}
// Parse end time to timestamp
try {
var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
var zonedDateTime = ZonedDateTime.parse(EndTime, formatter);
this.endTimeTimestamp = zonedDateTime.toInstant().toEpochMilli() / 1000;
} catch (Exception e) {
this.endTimeTimestamp = Long.MAX_VALUE;
}
}
}
@@ -17,9 +17,11 @@ public class BattlePassRewardDef extends BaseDef {
private int Qty2;
private int Tid3;
private int Qty3;
private boolean Focus;
private transient ItemParamMap basicRewards;
private transient ItemParamMap premiumRewards;
private transient ItemParamMap luxuryRewards;
@Override
public int getId() {
@@ -30,16 +32,26 @@ public class BattlePassRewardDef extends BaseDef {
public void onLoad() {
this.basicRewards = new ItemParamMap();
this.premiumRewards = new ItemParamMap();
this.luxuryRewards = new ItemParamMap();
// Basic rewards (Tid1) - for all players
if (this.Tid1 > 0) {
this.basicRewards.add(this.Tid1, this.Qty1);
}
// Premium rewards (Tid2) - for both 58 and 98 yuan tiers
if (this.Tid2 > 0) {
this.premiumRewards.add(this.Tid2, this.Qty2);
}
// Luxury rewards (Tid3) - ONLY for 98 yuan tier
if (this.Tid3 > 0) {
this.premiumRewards.add(this.Tid3, this.Qty3);
this.luxuryRewards.add(this.Tid3, this.Qty3);
}
}
public boolean hasLuxuryRewards() {
return this.luxuryRewards != null && !this.luxuryRewards.isEmpty();
}
}
@@ -4,6 +4,9 @@ import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.proto.MallGemListOuterClass.GemInfo;
import lombok.Getter;
@Getter
@@ -11,13 +14,51 @@ import lombok.Getter;
public class MallGemDef extends BaseDef {
@SerializedName("Id")
private String IdString;
private int Stock;
private int ItemId;
private int CurrencyItemId;
private int ItemQty;
private int BaseItemId;
private int BaseItemQty;
private int ExperiencedBonusItemId;
private int ExperiencedBonusItemQty;
private int MaidenBonusItemID;
private int MaidenBonusItemQty;
private int Price;
@Override
public int getId() {
return IdString.hashCode();
}
/**
* Builds the actual delivery payload for the current player state.
*/
public ItemParamMap buildProducts(Player player) {
var products = new ItemParamMap();
if (BaseItemId > 0 && BaseItemQty > 0) {
products.add(BaseItemId, BaseItemQty);
}
if (this.hasMaidenBonus(player)) {
if (MaidenBonusItemID > 0 && MaidenBonusItemQty > 0) {
products.add(MaidenBonusItemID, MaidenBonusItemQty);
}
return products;
}
if (ExperiencedBonusItemId > 0 && ExperiencedBonusItemQty > 0) {
products.add(ExperiencedBonusItemId, ExperiencedBonusItemQty);
}
return products;
}
public boolean hasMaidenBonus(Player player) {
return player.getInventory().hasMallGemMaidenBonus(this.IdString);
}
public GemInfo toInfo(Player player) {
return GemInfo.newInstance()
.setId(this.getIdString())
.setMaiden(this.hasMaidenBonus(player));
}
}
@@ -3,7 +3,11 @@ package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.proto.MallMonthlycardList.MonthlyCardInfo;
import lombok.Getter;
@Getter
@@ -15,9 +19,81 @@ public class MallMonthlyCardDef extends BaseDef {
private int Price;
private int BaseItemId;
private int BaseItemQty;
private int MaxDays;
private transient ItemParamMap products;
private transient ItemParamMap dailyRewards;
@Override
public int getId() {
return IdString.hashCode();
}
/**
* Monthly card valid duration(days).
*/
public int getMonthlyCardDuration() {
return 30;
}
/**
* Monthly cards can be repurchased until the remaining duration exceeds the configured cap.
*/
public boolean canPurchase(Player player) {
return this.getRemainingDays(player) <= this.MaxDays;
}
/**
* Returns the player's current remaining duration for this monthly card.
*/
public int getRemainingDays(Player player) {
return player.getMonthlyCardRemainingDays(this.getIdString());
}
/**
* Returns whether today's monthly-card reward has already been claimed.
*/
public boolean hasReceivedRewardToday(Player player) {
return player.receivedMonthlyCardRewardToday(this.getIdString());
}
public ItemParamMap getProducts() {
if (products == null) {
products = new ItemParamMap();
// Add base items (initial purchase reward)
if (BaseItemId > 0 && BaseItemQty > 0) {
products.add(BaseItemId, BaseItemQty);
}
}
return products;
}
/**
* Returns the configured daily login rewards for this monthly card.
*/
public ItemParamMap getDailyRewards() {
if (this.dailyRewards == null) {
this.dailyRewards = new ItemParamMap();
for (var rewardData : GameData.getMonthlyCardRewardDataTable()) {
if (rewardData.getCardId() != this.MonthlyCardId) {
continue;
}
this.dailyRewards.add(rewardData.getRewards());
break;
}
}
return this.dailyRewards.clone();
}
public MonthlyCardInfo toInfo(Player player) {
return MonthlyCardInfo.newInstance()
.setId(this.getIdString())
.setRemaining(this.getRemainingDays(player))
.setReceived(this.hasReceivedRewardToday(player));
}
}
@@ -1,32 +1,121 @@
package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName;
import emu.nebula.GameConstants;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.Nebula;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.util.JsonUtils;
import emu.nebula.util.ResetCycle;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
@ResourceType(name = "MallPackage.json")
public class MallPackageDef extends BaseDef {
@SerializedName("Id")
private String IdString;
private int Stock;
private int CurrencyType;
private int CurrencyItemId;
private int CurrencyItemQty;
private int Tag;
private String Items;
private String ListTime;
private String DeListTime;
private int RefreshType;
private int OrderCondType;
private String OrderCondParams;
private int ListCondType;
private String ListCondParams;
private transient ItemParamMap products;
private transient long listTimeSeconds;
private transient long delistTimeSeconds;
private transient int[] orderCondParams;
private transient int[] listCondParams;
@Override
public int getId() {
return IdString.hashCode();
}
public int getStock(Player player) {
return Math.max(Stock - player.getInventory().getMallPackagePurchaseCount(this.IdString), 0);
}
/**
* Returns whether this package is a free mall package(CurrencyType = 3).
*/
public boolean isFreePackage() {
return this.CurrencyType == GameConstants.CURRENCY_TYPE_FREE;
}
/**
* Returns whether this package is a cash mall package(CurrencyType = 1).
*/
public boolean isCashPackage() {
return this.CurrencyType == GameConstants.CURRENCY_TYPE_CASH;
}
/**
* Returns whether this package is bought with Stellanite Lumina.
*/
public boolean isItemPackage() {
return this.CurrencyType == GameConstants.CURRENCY_TYPE_ITEM;
}
/**
* Checks whether the player can purchase this package.
*/
public boolean canPurchase(Player player) {
return this.isVisible(player)
&& this.getStock(player) > 0
&& ShopCondition.matches(player, this.OrderCondType, this.orderCondParams);
}
/**
* Returns whether this package should be included in the mall package list for the player.
*/
public boolean isVisible(Player player) {
long now = Nebula.getCurrentServerTime();
if (!this.shouldIgnoreSaleWindow()) {
if (this.listTimeSeconds > 0 && now < this.listTimeSeconds) {
return false;
}
if (this.delistTimeSeconds > 0 && now >= this.delistTimeSeconds) {
return false;
}
}
return ShopCondition.matches(player, this.ListCondType, this.listCondParams);
}
/**
* Display all skins via config
*/
private boolean shouldIgnoreSaleWindow() {
return this.Tag == GameConstants.TAG_SKIN && Nebula.getConfig().getServerOptions().isShowAllSkinInMall();
}
/**
* Returns the next relevant stock refresh time for this package in epoch seconds.
*/
public long getNextRefreshTime() {
return Utils.getNextResetTimeSeconds(this.RefreshType);
}
@Override
public void onLoad() {
this.products = ItemParamMap.fromJsonString(this.Items);
this.listTimeSeconds = Utils.dateToSeconds(this.ListTime);
this.delistTimeSeconds = Utils.dateToSeconds(this.DeListTime);
this.orderCondParams = JsonUtils.decode(this.OrderCondParams, int[].class);
this.listCondParams = JsonUtils.decode(this.ListCondParams, int[].class);
}
}
@@ -1,14 +1,18 @@
package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.util.Utils;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@ResourceType(name = "MallShop.json")
public class MallShopDef extends BaseDef {
@SerializedName("Id")
@@ -20,8 +24,13 @@ public class MallShopDef extends BaseDef {
private int ItemId;
private int ItemQty;
private String ListTime;
private String DeListTime;
private int RefreshType;
private transient ItemParamMap products;
private transient long listTimeSeconds;
private transient long delistTimeSeconds;
@Override
public int getId() {
@@ -29,15 +38,46 @@ public class MallShopDef extends BaseDef {
}
public int getStock(Player player) {
return Math.max(this.getStock() - player.getInventory().getMallBuyCount().get(this.getIdString()), 0);
// Not purchase count limit.
if (this.Stock <= 0) {
return GameConstants.UNLIMITED_STOCK;
}
return Math.max(Stock - player.getInventory().getMallShopPurchaseCount(this.getIdString()), 0);
}
/**
* Returns whether the item is currently within its configured mall list window.
*/
public boolean isVisible() {
long now = Nebula.getCurrentServerTime();
return (this.listTimeSeconds <= 0 || now >= this.listTimeSeconds)
&& (this.delistTimeSeconds <= 0 || now < this.delistTimeSeconds);
}
/**
* Returns whether the player can currently purchase the requested quantity.
*/
public boolean canPurchase(Player player, int quantity) {
return quantity > 0 && this.isVisible() && this.getStock(player) >= quantity;
}
/**
* Returns the next relevant stock refresh time for this mall shop entry in epoch seconds.
*/
public long getNextRefreshTime() {
return Utils.getNextResetTimeSeconds(this.RefreshType);
}
@Override
public void onLoad() {
this.products = new ItemParamMap();
this.listTimeSeconds = Utils.dateToSeconds(this.ListTime);
this.delistTimeSeconds = Utils.dateToSeconds(this.DeListTime);
if (this.ItemId > 0) {
this.products.add(this.ItemId, this.ItemQty);
}
}
}
@@ -0,0 +1,37 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import lombok.Getter;
@Getter
@ResourceType(name = "MonthlyCard.json")
public class MonthlyCardRewardDef extends BaseDef {
private int Id;
private int CardId;
private int RewardId1;
private int RewardNum1;
private int RewardId2;
private int RewardNum2;
private transient ItemParamMap rewards;
@Override
public int getId() {
return this.Id;
}
@Override
public void onLoad() {
this.rewards = new ItemParamMap();
if (this.RewardId1 > 0 && this.RewardNum1 > 0) {
this.rewards.add(this.RewardId1, this.RewardNum1);
}
if (this.RewardId2 > 0 && this.RewardNum2 > 0) {
this.rewards.add(this.RewardId2, this.RewardNum2);
}
}
}
@@ -4,6 +4,7 @@ import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.util.JsonUtils;
import lombok.Getter;
@Getter
@@ -18,24 +19,36 @@ public class ResidentGoodsDef extends BaseDef {
private int CurrencyItemId;
private int Price;
private int AppearCondType;
private String AppearCondParams;
private transient ItemParamMap products;
private transient int[] appearCondParams;
@Override
public int getId() {
return Id;
}
public int getStock(Player player) {
return Math.max(this.getMaximumLimit() - player.getInventory().getMallBuyCount().getInt(this.getId()), 0);
return Math.max(this.getMaximumLimit() - player.getInventory().getShopBuyCount().get(this.getId()), 0);
}
/**
* Returns whether this resident shop good should currently be visible.
*/
public boolean isVisible(Player player) {
return ShopCondition.matchesResidentGoodsAppear(player, this.AppearCondType, this.appearCondParams);
}
@Override
public void onLoad() {
this.products = new ItemParamMap();
this.appearCondParams = JsonUtils.decode(this.AppearCondParams, int[].class);
if (this.ItemId > 0) {
this.products.add(this.ItemId, this.ItemQuantity);
}
}
}
@@ -1,16 +1,47 @@
package emu.nebula.data.resources;
import emu.nebula.Nebula;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
@ResourceType(name = "ResidentShop.json")
public class ResidentShopDef extends BaseDef {
private int Id;
private int RefreshTimeType;
private int RefreshInterval;
private String OpenTime;
private transient long openTimeSeconds;
@Override
public int getId() {
return Id;
}
/**
* Returns the next refresh time for this resident shop in epoch seconds.
*/
public long getNextRefreshTime() {
if (this.openTimeSeconds > 0 && Nebula.getCurrentServerTime() < this.openTimeSeconds) {
return this.openTimeSeconds;
}
return Utils.getNextResetTimeSeconds(this.RefreshTimeType);
}
/**
* Returns whether this resident shop should currently be visible to the client.
*/
public boolean isVisible() {
return this.openTimeSeconds <= 0 || this.openTimeSeconds <= Nebula.getCurrentServerTime();
}
@Override
public void onLoad() {
this.openTimeSeconds = Utils.dateToSeconds(this.OpenTime);
}
}
@@ -0,0 +1,77 @@
package emu.nebula.data.resources;
import emu.nebula.data.GameData;
import emu.nebula.game.player.Player;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum ShopCondition {
None(0),
WorldClassSpecific(71),
ShopPreGoodsSellOut(85),
ActivityShopPreGoodsSellOut(115);
@Getter
private final int value;
private static final Int2ObjectMap<ShopCondition> map = new Int2ObjectOpenHashMap<>();
static {
for (ShopCondition type : ShopCondition.values()) {
map.put(type.getValue(), type);
}
}
ShopCondition(int value) {
this.value = value;
}
public static ShopCondition getByValue(int value) {
return map.get(value);
}
public static boolean matches(Player player, int condType, int[] condParams) {
ShopCondition condition = ShopCondition.getByValue(condType);
if (condition == ShopCondition.None) {
return true;
}
if (condition == null) {
return false;
}
if (condition == ShopCondition.WorldClassSpecific) {
int requiredLevel = condParams != null && condParams.length > 0 ? condParams[0] : 0;
return player.getLevel() >= requiredLevel;
}
return false;
}
public static boolean matchesResidentGoodsAppear(Player player, int condType, int[] condParams) {
ShopCondition condition = ShopCondition.getByValue(condType);
if (condition == ShopCondition.None) {
return true;
}
if (condition == null) {
return false;
}
if (condition == ShopCondition.ShopPreGoodsSellOut) {
if (condParams == null || condParams.length < 2) {
return false;
}
int requiredGoodsId = condParams[1];
ResidentGoodsDef requiredGoods = GameData.getResidentGoodsDataTable().get(requiredGoodsId);
if (requiredGoods == null) {
return false;
}
int boughtCount = player.getInventory().getShopBuyCount().get(requiredGoodsId);
return boughtCount > 0 && boughtCount >= requiredGoods.getMaximumLimit();
}
return false;
}
}