mirror of
https://github.com/Melledy/Nebula.git
synced 2026-09-20 01:29:54 +02:00
Fix mall, shop, mall/shop refresh logic and battle pass flows
This commit is contained in:
@@ -1,21 +1,20 @@
|
||||
package emu.nebula.game;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.game.activity.ActivityModule;
|
||||
import emu.nebula.game.ban.BanModule;
|
||||
import emu.nebula.game.gacha.GachaModule;
|
||||
import emu.nebula.game.jointdrill.JointDrillModule;
|
||||
import emu.nebula.game.mall.PayModule;
|
||||
import emu.nebula.game.player.PlayerModule;
|
||||
import emu.nebula.game.scoreboss.ScoreBossModule;
|
||||
import emu.nebula.game.tutorial.TutorialModule;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.util.ResetCycle;
|
||||
import emu.nebula.util.Utils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
@@ -34,14 +33,15 @@ public class GameContext implements Runnable {
|
||||
private final ScoreBossModule scoreBossModule;
|
||||
private final JointDrillModule jointDrillModule;
|
||||
private final BanModule banModule;
|
||||
private final PayModule payModule;
|
||||
|
||||
// Game loop
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
// Daily
|
||||
private long epochDays;
|
||||
private int epochWeeks;
|
||||
private int epochMonths;
|
||||
// Reset-period indexes aligned to the configured daily reset boundary.
|
||||
private long resetDays;
|
||||
private int resetWeeks;
|
||||
private int resetMonths;
|
||||
|
||||
public GameContext() {
|
||||
// Create session map
|
||||
@@ -55,6 +55,7 @@ public class GameContext implements Runnable {
|
||||
this.scoreBossModule = new ScoreBossModule(this);
|
||||
this.jointDrillModule = new JointDrillModule(this);
|
||||
this.banModule = new BanModule(this);
|
||||
this.payModule = new PayModule(this);
|
||||
|
||||
// Run game loop
|
||||
this.scheduler = Executors.newScheduledThreadPool(1);
|
||||
@@ -76,7 +77,7 @@ public class GameContext implements Runnable {
|
||||
}
|
||||
|
||||
// Generate token
|
||||
String token = null;
|
||||
String token;
|
||||
|
||||
do {
|
||||
token = session.generateToken();
|
||||
@@ -107,20 +108,14 @@ public class GameContext implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Check daily - Update epoch days
|
||||
long offset = Nebula.getConfig().getServerOptions().getDailyResetHour() * -3600;
|
||||
var instant = Instant.now().plusSeconds(offset);
|
||||
var date = LocalDate.ofInstant(instant, GameConstants.UTC_ZONE);
|
||||
|
||||
// Update epoch days
|
||||
long lastEpochDays = this.epochDays;
|
||||
this.epochDays = date.toEpochDay();
|
||||
long lastResetDays = this.resetDays;
|
||||
this.resetDays = Utils.getResetEpochDay();
|
||||
|
||||
// Check if the day was changed
|
||||
if (this.epochDays > lastEpochDays) {
|
||||
if (this.resetDays > lastResetDays) {
|
||||
// Update epoch weeks/months
|
||||
this.epochWeeks = Utils.getWeeks(this.epochDays);
|
||||
this.epochMonths = Utils.getMonths(this.epochDays);
|
||||
this.resetWeeks = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, Nebula.getCurrentServerTime());
|
||||
this.resetMonths = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, Nebula.getCurrentServerTime());
|
||||
|
||||
// Update score boss season
|
||||
this.getScoreBossModule().updateSeason();
|
||||
@@ -145,8 +140,7 @@ public class GameContext implements Runnable {
|
||||
if (player == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
player.checkResetDailies();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +148,12 @@ public enum AchievementCondition {
|
||||
}
|
||||
}
|
||||
|
||||
private AchievementCondition(int value) {
|
||||
AchievementCondition(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static AchievementCondition getByValue(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package emu.nebula.game.battlepass;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import dev.morphia.annotations.Id;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.data.resources.BattlePassDef;
|
||||
import emu.nebula.data.resources.BattlePassRewardDef;
|
||||
import emu.nebula.database.GameDatabaseObject;
|
||||
import emu.nebula.game.inventory.ItemParamMap;
|
||||
@@ -20,10 +16,15 @@ import emu.nebula.game.quest.QuestType;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.BattlePassInfoOuterClass.BattlePassInfo;
|
||||
import emu.nebula.util.Bitset;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity(value = "battlepass", useDiscriminator = false)
|
||||
public class BattlePass implements GameDatabaseObject {
|
||||
@Id
|
||||
@@ -49,7 +50,7 @@ public class BattlePass implements GameDatabaseObject {
|
||||
public BattlePass(BattlePassManager manager) {
|
||||
this.uid = manager.getPlayerUid();
|
||||
this.manager = manager;
|
||||
this.battlePassId = GameConstants.BATTLE_PASS_ID;
|
||||
this.battlePassId = getActiveBattlePassId();
|
||||
this.basicReward = new Bitset();
|
||||
this.premiumReward = new Bitset();
|
||||
|
||||
@@ -63,8 +64,22 @@ public class BattlePass implements GameDatabaseObject {
|
||||
this.save();
|
||||
}
|
||||
|
||||
public void setManager(BattlePassManager manager) {
|
||||
this.manager = manager;
|
||||
private static int getActiveBattlePassId() {
|
||||
long currentTimeSec = Nebula.getCurrentServerTime();
|
||||
|
||||
for (BattlePassDef data : GameData.getBattlePassDataTable()) {
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long start = data.getStartTimeTimestamp();
|
||||
long end = data.getEndTimeTimestamp();
|
||||
if (currentTimeSec >= start && currentTimeSec <= end) {
|
||||
return data.getId();
|
||||
}
|
||||
}
|
||||
|
||||
return GameConstants.BATTLE_PASS_ID;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
@@ -116,30 +131,63 @@ public class BattlePass implements GameDatabaseObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any rewards or quests are claimable
|
||||
* Check claimable tasks to show battle pass red dot.
|
||||
*/
|
||||
public synchronized boolean hasNew() {
|
||||
// Check if any quests are complete but unclaimed
|
||||
public synchronized boolean hasClaimableQuest() {
|
||||
if (!this.getPlayer().isBattlePassUnlocked()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var nextLevelData = GameData.getBattlePassLevelDataTable().get(this.getLevel() + 1);
|
||||
if (nextLevelData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var quest : getQuests().values()) {
|
||||
if (quest.isComplete() && !quest.isClaimed()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have any pending rewards
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the battle pass currently has at least one claimable
|
||||
* reward lane item for the player's current level/mode.
|
||||
*/
|
||||
public synchronized boolean hasClaimableReward() {
|
||||
if (!this.getPlayer().isBattlePassUnlocked()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= this.getLevel(); i++) {
|
||||
if (!this.getBasicReward().isSet(i)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (this.isPremium() && !this.getPremiumReward().isSet(i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No claimable things
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the client battle-pass state red dot contract:
|
||||
* 0 = none, 1 = quest only, 2 = reward only, 3 = both.
|
||||
*/
|
||||
public synchronized int getClientState() {
|
||||
int state = 0;
|
||||
if (this.hasClaimableQuest()) {
|
||||
state |= 1;
|
||||
}
|
||||
if (this.hasClaimableReward()) {
|
||||
state |= 2;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
public synchronized void resetDailyQuests(boolean resetWeekly) {
|
||||
// Reset daily quests
|
||||
@@ -190,7 +238,7 @@ public class BattlePass implements GameDatabaseObject {
|
||||
* Update this quest on the player client
|
||||
*/
|
||||
private void syncQuest(GameQuest quest) {
|
||||
if (!getPlayer().hasSession()) {
|
||||
if (!getPlayer().hasSession() || !getPlayer().isBattlePassUnlocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -269,7 +317,7 @@ public class BattlePass implements GameDatabaseObject {
|
||||
|
||||
public PlayerChangeInfo receiveReward(boolean premium, int levelId) {
|
||||
// Get bitset
|
||||
Bitset rewards = null;
|
||||
Bitset rewards;
|
||||
|
||||
if (premium) {
|
||||
rewards = this.getPremiumReward();
|
||||
@@ -296,7 +344,13 @@ public class BattlePass implements GameDatabaseObject {
|
||||
|
||||
// Add items
|
||||
if (premium) {
|
||||
return getPlayer().getInventory().addItems(data.getPremiumRewards());
|
||||
var premiumRewards = data.getPremiumRewards().clone();
|
||||
|
||||
if (this.getMode() >= 2 && data.hasLuxuryRewards()) {
|
||||
premiumRewards.add(data.getLuxuryRewards());
|
||||
}
|
||||
|
||||
return getPlayer().getInventory().addItems(premiumRewards);
|
||||
} else {
|
||||
return getPlayer().getInventory().addItems(data.getBasicRewards());
|
||||
}
|
||||
@@ -308,38 +362,29 @@ public class BattlePass implements GameDatabaseObject {
|
||||
|
||||
// Get unclaimed rewards
|
||||
for (int i = 1; i <= this.getLevel(); i++) {
|
||||
// Cache reward data
|
||||
BattlePassRewardDef data = null;
|
||||
|
||||
// Basic reward
|
||||
if (!this.getBasicReward().isSet(i)) {
|
||||
// Set flag
|
||||
this.getBasicReward().setBit(i);
|
||||
|
||||
// Get reward data if we havent already
|
||||
if (data == null) {
|
||||
data = this.getRewardData(i);
|
||||
}
|
||||
|
||||
// Add basic rewards
|
||||
if (data != null) {
|
||||
rewards.add(data.getBasicRewards());
|
||||
}
|
||||
boolean claimBasic = !this.getBasicReward().isSet(i);
|
||||
boolean claimPremium = this.isPremium() && !this.getPremiumReward().isSet(i);
|
||||
|
||||
if (!claimBasic && !claimPremium) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Premium reward
|
||||
if (this.isPremium() && !this.getPremiumReward().isSet(i)) {
|
||||
// Set flag
|
||||
|
||||
BattlePassRewardDef data = this.getRewardData(i);
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (claimBasic) {
|
||||
this.getBasicReward().setBit(i);
|
||||
rewards.add(data.getBasicRewards());
|
||||
}
|
||||
|
||||
if (claimPremium) {
|
||||
this.getPremiumReward().setBit(i);
|
||||
|
||||
// Get reward data if we havent already
|
||||
if (data == null) {
|
||||
data = this.getRewardData(i);
|
||||
}
|
||||
|
||||
// Add basic rewards
|
||||
if (data != null) {
|
||||
rewards.add(data.getPremiumRewards());
|
||||
rewards.add(data.getPremiumRewards());
|
||||
|
||||
if (this.getMode() >= 2 && data.hasLuxuryRewards()) {
|
||||
rewards.add(data.getLuxuryRewards());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,6 +403,20 @@ public class BattlePass implements GameDatabaseObject {
|
||||
// Proto
|
||||
|
||||
public BattlePassInfo toProto() {
|
||||
// Return a locked/empty snapshot until the battle pass feature is unlocked,
|
||||
// so the client cannot derive local quest or reward red dots from battle pass data.
|
||||
if (!this.getPlayer().isBattlePassUnlocked()) {
|
||||
return BattlePassInfo.newInstance()
|
||||
.setId(0)
|
||||
.setLevel(0)
|
||||
.setMode(0)
|
||||
.setExp(0)
|
||||
.setExpThisWeek(0)
|
||||
.setDeadline(0L)
|
||||
.setBasicReward()
|
||||
.setPremiumReward();
|
||||
}
|
||||
|
||||
var proto = BattlePassInfo.newInstance()
|
||||
.setId(this.getBattlePassId())
|
||||
.setLevel(this.getLevel())
|
||||
@@ -367,10 +426,10 @@ public class BattlePass implements GameDatabaseObject {
|
||||
.setDeadline(Long.MAX_VALUE)
|
||||
.setBasicReward(this.getBasicReward().toByteArray())
|
||||
.setPremiumReward(this.getPremiumReward().toByteArray());
|
||||
|
||||
|
||||
var daily = proto.getMutableDailyQuests();
|
||||
var weekly = proto.getMutableWeeklyQuests();
|
||||
|
||||
|
||||
for (var quest : this.getQuests().values()) {
|
||||
if (quest.getType() == QuestType.BattlePassDaily) {
|
||||
daily.addList(quest.toProto());
|
||||
@@ -378,7 +437,8 @@ public class BattlePass implements GameDatabaseObject {
|
||||
weekly.addList(quest.toProto());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ public class BattlePassManager extends PlayerManager {
|
||||
public BattlePassManager(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
public boolean hasNew() {
|
||||
return this.getBattlePass().hasNew();
|
||||
}
|
||||
|
||||
// Database
|
||||
|
||||
|
||||
@@ -1,43 +1,37 @@
|
||||
package emu.nebula.game.inventory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.mongodb.client.model.Filters;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import dev.morphia.annotations.Id;
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.data.resources.DropPkgDef;
|
||||
import emu.nebula.data.resources.MallShopDef;
|
||||
import emu.nebula.data.resources.ResidentGoodsDef;
|
||||
import emu.nebula.data.resources.*;
|
||||
import emu.nebula.database.GameDatabaseObject;
|
||||
import emu.nebula.game.achievement.AchievementCondition;
|
||||
import emu.nebula.game.player.Player;
|
||||
import emu.nebula.game.player.PlayerChangeInfo;
|
||||
import emu.nebula.game.player.PlayerManager;
|
||||
import emu.nebula.game.quest.QuestCondition;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.Notify.Skin;
|
||||
import emu.nebula.proto.Public.Honor;
|
||||
import emu.nebula.proto.Public.Item;
|
||||
import emu.nebula.proto.Public.Res;
|
||||
import emu.nebula.proto.Public.Title;
|
||||
import emu.nebula.proto.Public.UI32;
|
||||
import emu.nebula.proto.Public.*;
|
||||
import emu.nebula.util.Utils;
|
||||
import emu.nebula.util.ints.String2IntMap;
|
||||
import emu.nebula.game.achievement.AchievementCondition;
|
||||
import emu.nebula.game.player.Player;
|
||||
import emu.nebula.game.player.PlayerChangeInfo;
|
||||
import it.unimi.dsi.fastutil.ints.IntCollection;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.lang.String;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Entity(value = "inventory", useDiscriminator = false)
|
||||
public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
@Id
|
||||
private int uid;
|
||||
|
||||
|
||||
// Items/resources
|
||||
private ItemParamMap items;
|
||||
private ItemParamMap resources;
|
||||
@@ -51,8 +45,15 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
// Buy limit
|
||||
private ItemParamMap shopBuyCount;
|
||||
private String2IntMap mallBuyCount;
|
||||
|
||||
@Deprecated
|
||||
private String2IntMap mallPackageBuyCount;
|
||||
/**
|
||||
* Tracks whether a MallGem pack has already consumed its maiden bonus.
|
||||
* This is the only persisted state MallGem recharge still needs now that
|
||||
* recharge packs no longer have any purchase-limit semantics.
|
||||
*/
|
||||
private String2IntMap gemMaidenClaimed;
|
||||
private String2IntMap monthlyCardBuyCount;
|
||||
|
||||
public Inventory() {
|
||||
// Morphia only
|
||||
}
|
||||
@@ -73,7 +74,10 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
|
||||
this.shopBuyCount = new ItemParamMap();
|
||||
this.mallBuyCount = new String2IntMap();
|
||||
|
||||
this.mallPackageBuyCount = new String2IntMap();
|
||||
this.gemMaidenClaimed = new String2IntMap();
|
||||
this.monthlyCardBuyCount = new String2IntMap();
|
||||
|
||||
// Add player heads
|
||||
this.getHeadIcons().add(101);
|
||||
this.getHeadIcons().add(102);
|
||||
@@ -562,6 +566,29 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
change.add(proto);
|
||||
}
|
||||
}
|
||||
case HeadItem -> {
|
||||
// Cannot remove head icons
|
||||
if (amount <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Ensure the head icon exists in data and persist ownership.
|
||||
var headData = GameData.getPlayerHeadDataTable().get(id);
|
||||
if (headData == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Persist ownership and expose it in ChangeInfo as a normal item reward,
|
||||
// so regular reward popup can still show the head icon entry.
|
||||
if (this.addHeadIcon(id)) {
|
||||
// Client head-page state/red-dot updates are driven by proto.HeadIcon
|
||||
// in ChangeInfo, not by a generic proto.Item entry.
|
||||
var proto = HeadIcon.newInstance()
|
||||
.setTid(id);
|
||||
|
||||
change.add(proto);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
// Not implemented
|
||||
}
|
||||
@@ -651,7 +678,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player has enough quanity of this item
|
||||
* Checks if the player has enough quantity of this item
|
||||
*/
|
||||
public synchronized boolean hasItem(int id, int count) {
|
||||
// Sanity check
|
||||
@@ -726,7 +753,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
}
|
||||
|
||||
// Get materials
|
||||
var materials = data.getMaterials().mulitply(num);
|
||||
var materials = data.getMaterials().multiply(num);
|
||||
|
||||
// Verify that we have the materials
|
||||
if (!this.hasItems(materials)) {
|
||||
@@ -774,8 +801,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
|
||||
public PlayerChangeInfo buyMallItem(MallShopDef data, int buyCount) {
|
||||
// Check stock
|
||||
int stock = data.getStock(this.getPlayer());
|
||||
if (buyCount > stock) {
|
||||
if (!data.canPurchase(this.getPlayer(), buyCount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -787,14 +813,14 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
}
|
||||
|
||||
// Update purchase limit
|
||||
this.getMallBuyCount().addTo(data.getIdString(), buyCount);
|
||||
int purchaseCount = this.getMallShopPurchaseCount(data.getIdString());
|
||||
this.setMallCounterValue(this.getMallBuyCount(), data.getIdString(), purchaseCount + buyCount);
|
||||
Nebula.getGameDatabase().update(
|
||||
this,
|
||||
getUid(),
|
||||
"mallBuyCount." + data.getIdString(),
|
||||
getMallBuyCount().get(data.getIdString())
|
||||
);
|
||||
|
||||
this,
|
||||
getUid(),
|
||||
"mallBuyCount",
|
||||
this.getMallBuyCount());
|
||||
|
||||
// Return
|
||||
return change;
|
||||
}
|
||||
@@ -846,7 +872,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
this.removeItem(currencyId, cost, change);
|
||||
|
||||
// Add items
|
||||
this.addItems(buyItems.mulitply(buyCount), change);
|
||||
this.addItems(buyItems.multiply(buyCount), change);
|
||||
|
||||
// Success
|
||||
return change.setSuccess(true);
|
||||
@@ -879,7 +905,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
switch (data.getUseAction()) {
|
||||
case 2 -> {
|
||||
// Add items
|
||||
this.addItems(data.getUseParams().mulitply(count), change);
|
||||
this.addItems(data.getUseParams().multiply(count), change);
|
||||
|
||||
// Success
|
||||
success = true;
|
||||
@@ -912,18 +938,18 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
return change.setSuccess(true);
|
||||
}
|
||||
|
||||
public PlayerChangeInfo convertGems(int amount) {
|
||||
// Verify that we have the gems
|
||||
if (!this.hasItem(GameConstants.PREM_GEM_ITEM_ID, amount)) {
|
||||
public PlayerChangeInfo convertStellaniteLuminaToDust(int amount) {
|
||||
// Verify that we have enough stellanite lumina in the combined paid/free pool.
|
||||
if (!this.hasMallPackageCurrency(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID, amount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create change info
|
||||
var change = new PlayerChangeInfo();
|
||||
|
||||
// Convert gems
|
||||
this.removeItem(GameConstants.PREM_GEM_ITEM_ID, amount, change);
|
||||
this.addItem(GameConstants.GEM_ITEM_ID, amount, change);
|
||||
// Convert stellanite lumina into stellanite dust.
|
||||
this.consumeMallPackageCurrency(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID, amount, change);
|
||||
this.addItem(GameConstants.STELLANITE_DUST_ITEM_ID, amount, change);
|
||||
|
||||
// Success
|
||||
return change.setSuccess(true);
|
||||
@@ -942,7 +968,207 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), "mallBuyCount", this.getMallBuyCount());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void resetWeeklyMallPackagePurchases() {
|
||||
this.resetMallCounterByRefreshType(this.getMallPackageBuyCount(), "mallPackageBuyCount", GameConstants.REFRESH_TYPE_WEEKLY);
|
||||
}
|
||||
|
||||
public void resetMonthlyMallPackagePurchases() {
|
||||
this.resetMallCounterByRefreshType(this.getMallPackageBuyCount(), "mallPackageBuyCount", GameConstants.REFRESH_TYPE_MONTHLY);
|
||||
}
|
||||
|
||||
private void resetMallCounterByRefreshType(String2IntMap counter, String fieldName, int refreshType) {
|
||||
if (counter == null || counter.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var updated = new String2IntMap();
|
||||
boolean changed = false;
|
||||
|
||||
for (var entry : counter.object2IntEntrySet()) {
|
||||
var data = GameData.getMallPackageDataTable().get(Utils.unescapeKey(entry.getKey()).hashCode());
|
||||
|
||||
if (data != null && data.getRefreshType() == refreshType) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
updated.put(entry.getKey(), entry.getIntValue());
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
counter.clear();
|
||||
counter.putAll(updated);
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), fieldName, counter);
|
||||
}
|
||||
|
||||
private int getMallCounterValue(String2IntMap counter, String key) {
|
||||
if (counter == null || key == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int directValue = counter.get(key);
|
||||
String escapedKey = Utils.escapeKey(key);
|
||||
|
||||
if (escapedKey.equals(key)) {
|
||||
return directValue;
|
||||
}
|
||||
|
||||
return Math.max(directValue, counter.get(escapedKey));
|
||||
}
|
||||
|
||||
private void setMallCounterValue(String2IntMap counter, String key, int value) {
|
||||
if (counter == null || key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String escapedKey = Utils.escapeKey(key);
|
||||
counter.put(escapedKey, value);
|
||||
|
||||
if (!escapedKey.equals(key)) {
|
||||
counter.removeInt(key);
|
||||
}
|
||||
}
|
||||
|
||||
public int getMallPackagePurchaseCount(String packageId) {
|
||||
return this.getMallCounterValue(this.getMallPackageBuyCount(), packageId);
|
||||
}
|
||||
|
||||
public int getMallShopPurchaseCount(String shopId) {
|
||||
return this.getMallCounterValue(this.getMallBuyCount(), shopId);
|
||||
}
|
||||
|
||||
public boolean hasMallGemMaidenBonus(String gemId) {
|
||||
return this.getMallCounterValue(this.getGemMaidenClaimed(), gemId) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated toggle for MallGem first-purchase bonus state.
|
||||
* Can reset this field later to reopen maiden bonus eligibility
|
||||
*/
|
||||
public void setMallGemMaidenClaimed(String gemId, boolean claimed) {
|
||||
if (gemId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.gemMaidenClaimed == null) {
|
||||
this.gemMaidenClaimed = new String2IntMap();
|
||||
}
|
||||
|
||||
if (claimed) {
|
||||
this.setMallCounterValue(this.getGemMaidenClaimed(), gemId, 1);
|
||||
} else {
|
||||
this.gemMaidenClaimed.removeInt(Utils.escapeKey(gemId));
|
||||
this.gemMaidenClaimed.removeInt(gemId);
|
||||
}
|
||||
Nebula.getGameDatabase().update(this, getUid(), "gemMaidenClaimed", this.getGemMaidenClaimed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all MallGem maiden-bonus flags.
|
||||
* This is the intended operation hook for anniversary-style first-purchase refreshes.
|
||||
*/
|
||||
public void resetMallGemMaidenBonuses() {
|
||||
if (this.gemMaidenClaimed == null || this.gemMaidenClaimed.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gemMaidenClaimed.clear();
|
||||
Nebula.getGameDatabase().update(this, getUid(), "gemMaidenClaimed", this.getGemMaidenClaimed());
|
||||
}
|
||||
|
||||
public int getMallMonthlyCardPurchaseCount(String cardId) {
|
||||
return this.getMallCounterValue(this.getMonthlyCardBuyCount(), cardId);
|
||||
}
|
||||
|
||||
public PlayerChangeInfo buyMallPackage(MallPackageDef data) {
|
||||
var player = this.getPlayer();
|
||||
if (!data.canPurchase(player)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int currencyItemId = data.getCurrencyItemId();
|
||||
int currencyItemQty = data.getCurrencyItemQty();
|
||||
if (data.isItemPackage() && currencyItemId > 0 && currencyItemQty > 0
|
||||
&& !this.hasMallPackageCurrency(currencyItemId, currencyItemQty)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var change = new PlayerChangeInfo();
|
||||
|
||||
if (data.isItemPackage() && currencyItemId > 0 && currencyItemQty > 0) {
|
||||
this.consumeMallPackageCurrency(currencyItemId, currencyItemQty, change);
|
||||
}
|
||||
|
||||
this.addItems(data.getProducts(), change);
|
||||
|
||||
String packageId = data.getIdString();
|
||||
int purchaseCount = this.getMallPackagePurchaseCount(packageId);
|
||||
this.setMallCounterValue(this.getMallPackageBuyCount(), packageId, purchaseCount + 1);
|
||||
Nebula.getGameDatabase().update(this, getUid(), "mallPackageBuyCount", this.getMallPackageBuyCount());
|
||||
|
||||
return change.setSuccess(true);
|
||||
}
|
||||
|
||||
private boolean hasMallPackageCurrency(int currencyItemId, int currencyItemQty) {
|
||||
if (currencyItemQty <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currencyItemId == GameConstants.PAID_STELLANITE_LUMINA_ITEM_ID
|
||||
|| currencyItemId == GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID) {
|
||||
return this.getResourceCount(GameConstants.PAID_STELLANITE_LUMINA_ITEM_ID)
|
||||
+ this.getResourceCount(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID) >= currencyItemQty;
|
||||
}
|
||||
|
||||
return this.hasItem(currencyItemId, currencyItemQty);
|
||||
}
|
||||
|
||||
private void consumeMallPackageCurrency(int currencyItemId, int currencyItemQty, PlayerChangeInfo change) {
|
||||
if (currencyItemId == GameConstants.PAID_STELLANITE_LUMINA_ITEM_ID
|
||||
|| currencyItemId == GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID) {
|
||||
// use free stellanite lumina first.
|
||||
int freeOwned = this.getResourceCount(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID);
|
||||
int useFree = Math.min(freeOwned, currencyItemQty);
|
||||
if (useFree > 0) {
|
||||
this.removeItem(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID, useFree, change);
|
||||
}
|
||||
|
||||
int remain = currencyItemQty - useFree;
|
||||
if (remain > 0) {
|
||||
this.removeItem(GameConstants.PAID_STELLANITE_LUMINA_ITEM_ID, remain, change);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeItem(currencyItemId, currencyItemQty, change);
|
||||
}
|
||||
|
||||
public PlayerChangeInfo buyMallGem(MallGemDef data) {
|
||||
var change = new PlayerChangeInfo();
|
||||
|
||||
this.addItems(data.buildProducts(this.getPlayer()), change);
|
||||
this.setMallGemMaidenClaimed(data.getIdString(), true);
|
||||
|
||||
return change.setSuccess(true);
|
||||
}
|
||||
|
||||
public PlayerChangeInfo buyMallMonthlyCard(MallMonthlyCardDef data) {
|
||||
var change = new PlayerChangeInfo();
|
||||
|
||||
this.addItems(data.getProducts(), change);
|
||||
|
||||
String cardId = data.getIdString();
|
||||
int purchaseCount = this.getMallMonthlyCardPurchaseCount(cardId);
|
||||
this.setMallCounterValue(this.getMonthlyCardBuyCount(), cardId, purchaseCount + 1);
|
||||
Nebula.getGameDatabase().update(this, getUid(), "monthlyCardBuyCount", this.getMonthlyCardBuyCount());
|
||||
|
||||
return change.setSuccess(true);
|
||||
}
|
||||
|
||||
// Database
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@@ -990,7 +1216,22 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
// Set to save inventory to database
|
||||
save = true;
|
||||
}
|
||||
|
||||
|
||||
if (this.mallPackageBuyCount == null) {
|
||||
this.mallPackageBuyCount = new String2IntMap();
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (this.gemMaidenClaimed == null) {
|
||||
this.gemMaidenClaimed = new String2IntMap();
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (this.monthlyCardBuyCount == null) {
|
||||
this.monthlyCardBuyCount = new String2IntMap();
|
||||
save = true;
|
||||
}
|
||||
|
||||
// Update in database
|
||||
if (save) {
|
||||
this.save();
|
||||
|
||||
@@ -54,10 +54,10 @@ public class ItemParamMap extends Int2IntLinkedOpenHashMap implements ObjectBidi
|
||||
|
||||
/**
|
||||
* Returns a new ItemParamMap with item amounts multiplied
|
||||
* @param mult Value to multiply all item amounts in this map by
|
||||
* @return
|
||||
* @param multiplier Value to multiply all item amounts in this map by
|
||||
* @return ItemParamMap
|
||||
*/
|
||||
public ItemParamMap mulitply(int multiplier) {
|
||||
public ItemParamMap multiply(int multiplier) {
|
||||
var params = new ItemParamMap();
|
||||
|
||||
for (var entry : this.int2IntEntrySet()) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package emu.nebula.game.mall;
|
||||
|
||||
import emu.nebula.proto.Public.ChangeInfo;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Separate real reward delta from popup display data
|
||||
* Decouple settlement logic from client UI, prevent data mutation on reuse
|
||||
*/
|
||||
@Getter
|
||||
public final class MallOrderCollectResult {
|
||||
private final ChangeInfo stateChange;
|
||||
private final ChangeInfo displayChange;
|
||||
|
||||
private MallOrderCollectResult(ChangeInfo stateChange, ChangeInfo displayChange) {
|
||||
this.stateChange = copyOf(stateChange);
|
||||
this.displayChange = copyOf(displayChange);
|
||||
}
|
||||
|
||||
public static MallOrderCollectResult empty() {
|
||||
return new MallOrderCollectResult(null, null);
|
||||
}
|
||||
|
||||
public static MallOrderCollectResult of(ChangeInfo change) {
|
||||
return new MallOrderCollectResult(change, change);
|
||||
}
|
||||
|
||||
public static MallOrderCollectResult split(ChangeInfo stateChange, ChangeInfo displayChange) {
|
||||
return new MallOrderCollectResult(stateChange, displayChange);
|
||||
}
|
||||
|
||||
public boolean hasStateChange() {
|
||||
return !stateChange.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasDisplayChange() {
|
||||
return !displayChange.isEmpty();
|
||||
}
|
||||
|
||||
private static ChangeInfo copyOf(ChangeInfo change) {
|
||||
if (change == null || change.isEmpty()) {
|
||||
return ChangeInfo.newInstance();
|
||||
}
|
||||
|
||||
return ChangeInfo.newInstance().copyFrom(change);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package emu.nebula.game.mall;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.resources.MallPackageDef;
|
||||
import emu.nebula.data.resources.MallGemDef;
|
||||
import emu.nebula.data.resources.MallMonthlyCardDef;
|
||||
import emu.nebula.data.resources.BattlePassDef;
|
||||
import emu.nebula.data.resources.DropPkgDef;
|
||||
import emu.nebula.game.GameContext;
|
||||
import emu.nebula.game.GameContextModule;
|
||||
import emu.nebula.game.inventory.ItemParamMap;
|
||||
import emu.nebula.game.player.PlayerChangeInfo;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.net.PacketHelper;
|
||||
import emu.nebula.proto.MallGemOrder.OrderInfo;
|
||||
import emu.nebula.proto.Public.HeadIcon;
|
||||
import emu.nebula.proto.Notify.OrderStateChange;
|
||||
import emu.nebula.proto.Public.Item;
|
||||
import emu.nebula.proto.Public.ChangeInfo;
|
||||
import emu.nebula.proto.Public.Res;
|
||||
import emu.nebula.util.JsonUtils;
|
||||
|
||||
/**
|
||||
* Mock payment, isolate mock order data
|
||||
*/
|
||||
public class PayModule extends GameContextModule {
|
||||
|
||||
public PayModule(GameContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface CollectSettlement {
|
||||
MallOrderCollectResult resolve(GameSession session) throws Exception;
|
||||
}
|
||||
|
||||
private final ConcurrentHashMap<Integer, CollectSettlement> pendingCollects = new ConcurrentHashMap<>();
|
||||
private volatile Map<Integer, int[]> battlePassDropPackages;
|
||||
|
||||
public void clearPendingCollect(int uid) {
|
||||
pendingCollects.remove(uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the authoritative collect payload and clears the transient order state.
|
||||
*/
|
||||
public MallOrderCollectResult consumePendingCollect(GameSession session) throws Exception {
|
||||
CollectSettlement settlement = pendingCollects.remove(session.getPlayer().getUid());
|
||||
if (settlement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return settlement.resolve(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps mock-order flows aligned with normal inventory updates by reusing the
|
||||
* same item notify packets as the rest of the server.
|
||||
*/
|
||||
public void pushInventoryNotifies(GameSession session, ChangeInfo items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.getPlayer().addNextPackage(NetMsgId.items_change_notify, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use mock payment for gem recharge to match official mall order flow
|
||||
*/
|
||||
public OrderInfo createGemOrder(GameSession session, MallGemDef data) {
|
||||
return this.createOrder(session, "gem", data.getIdString(), collectSession -> {
|
||||
var change = collectSession.getPlayer().getInventory().buyMallGem(data);
|
||||
if (change == null) {
|
||||
return MallOrderCollectResult.empty();
|
||||
}
|
||||
|
||||
return MallOrderCollectResult.of(change.toProto());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer monthly card settlement to maintain consistent mock-payment lifecycle with mall flow.
|
||||
*/
|
||||
public OrderInfo createMonthlyCardOrder(GameSession session, MallMonthlyCardDef data) {
|
||||
var player = session.getPlayer();
|
||||
if (data == null || !data.canPurchase(player)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int durationDays = data.getMonthlyCardDuration();
|
||||
return this.createOrder(session, "monthly", data.getIdString(), collectSession -> {
|
||||
var collectPlayer = collectSession.getPlayer();
|
||||
var collectChange = collectPlayer.getInventory().buyMallMonthlyCard(data);
|
||||
if (collectChange == null) {
|
||||
return MallOrderCollectResult.empty();
|
||||
}
|
||||
|
||||
collectPlayer.activateMonthlyCard(data.getIdString(), durationDays);
|
||||
var dailyRewardChange = collectPlayer.createMonthlyCardRewardChange(data.getIdString());
|
||||
if (dailyRewardChange != null) {
|
||||
collectChange.add(dailyRewardChange);
|
||||
}
|
||||
|
||||
return MallOrderCollectResult.of(collectChange.toProto());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cash mall packages follow the same order -> paid notify -> collect lifecycle as other RMB products.
|
||||
*/
|
||||
public OrderInfo createPackageOrder(GameSession session, MallPackageDef data) {
|
||||
if (data == null || !data.isCashPackage() || !data.canPurchase(session.getPlayer())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.createOrder(session, "package", data.getIdString(), collectSession -> {
|
||||
var change = collectSession.getPlayer().getInventory().buyMallPackage(data);
|
||||
if (change == null) {
|
||||
return MallOrderCollectResult.empty();
|
||||
}
|
||||
|
||||
return MallOrderCollectResult.of(change.toProto());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create mock battle pass order following standard payment flow:
|
||||
* order confirm -> payment notify -> reward settlement.
|
||||
*/
|
||||
public OrderInfo createBattlePassOrder(GameSession session, BattlePassDef data, int requestedMode) {
|
||||
return this.createOrder(session, "battlepass", data.getId() + ":" + requestedMode, collectSession -> {
|
||||
var player = collectSession.getPlayer();
|
||||
var battlePass = player.getBattlePassManager().getBattlePass();
|
||||
if (battlePass == null) {
|
||||
return MallOrderCollectResult.empty();
|
||||
}
|
||||
|
||||
int currentMode = battlePass.getMode();
|
||||
if (requestedMode <= currentMode) {
|
||||
return MallOrderCollectResult.empty();
|
||||
}
|
||||
|
||||
battlePass.setMode(requestedMode);
|
||||
if (requestedMode == 2 && data.getLuxuryBonusLevel() > 0) {
|
||||
battlePass.setLevel(battlePass.getLevel() + data.getLuxuryBonusLevel());
|
||||
}
|
||||
|
||||
var rewards = new ItemParamMap();
|
||||
|
||||
int collectItemId = 0;
|
||||
int collectItemQty = 0;
|
||||
|
||||
// BattlePass.json semantics:
|
||||
// - mode=1 (Premium) unlocks premium claim lane only, no immediate collect reward.
|
||||
// - mode=2 (Luxury) grants immediate luxury reward.
|
||||
// - mode=1 -> mode=2 upgrade uses complementary reward config.
|
||||
if (requestedMode == 2) {
|
||||
if (currentMode == 0) {
|
||||
collectItemId = data.getLuxuryTid();
|
||||
collectItemQty = data.getLuxuryQty();
|
||||
} else if (currentMode == 1) {
|
||||
collectItemId = data.getComplementaryTid();
|
||||
collectItemQty = data.getComplementaryQty();
|
||||
}
|
||||
}
|
||||
|
||||
this.addBattlePassCollectRewards(rewards, collectItemId, collectItemQty);
|
||||
|
||||
battlePass.save();
|
||||
|
||||
if (rewards.isEmpty()) {
|
||||
return MallOrderCollectResult.empty();
|
||||
}
|
||||
|
||||
var change = player.getInventory().addItems(rewards);
|
||||
if (change == null || change.isEmpty()) {
|
||||
return MallOrderCollectResult.empty();
|
||||
}
|
||||
|
||||
var stateChange = change.toProto();
|
||||
var displayChange = this.buildBattlePassDisplayChange(change);
|
||||
return MallOrderCollectResult.split(stateChange, displayChange);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves battle-pass collect rewards.
|
||||
* <p>
|
||||
* Some battle-pass collect ids in Item.json are intermediate "drop keys" that
|
||||
* should expand to concrete rewards (for example, skin/head icon), otherwise
|
||||
* the client misses skin-gain specific reward presentation.
|
||||
*/
|
||||
private void addBattlePassCollectRewards(ItemParamMap rewards, int collectItemId, int collectItemQty) {
|
||||
if (collectItemId <= 0 || collectItemQty <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var packageIds = this.getBattlePassDropPackages().get(collectItemId);
|
||||
if (packageIds == null || packageIds.length == 0) {
|
||||
rewards.add(collectItemId, collectItemQty);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < collectItemQty; i++) {
|
||||
for (int packageId : packageIds) {
|
||||
int dropItemId = DropPkgDef.getRandomDrop(packageId);
|
||||
if (dropItemId > 0) {
|
||||
rewards.add(dropItemId, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads battle-pass drop-package mapping from Drop.json on first use and caches it.
|
||||
* Mapping rule: DropId -> [PkgId...].
|
||||
*/
|
||||
private Map<Integer, int[]> getBattlePassDropPackages() {
|
||||
var cache = this.battlePassDropPackages;
|
||||
if (cache != null) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
cache = this.battlePassDropPackages;
|
||||
if (cache != null) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
cache = this.loadBattlePassDropPackages();
|
||||
this.battlePassDropPackages = cache;
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Integer, int[]> loadBattlePassDropPackages() {
|
||||
final String dropPath = Nebula.getConfig().resourceDir + "/bin/Drop.json";
|
||||
var result = new LinkedHashMap<Integer, int[]>();
|
||||
|
||||
try {
|
||||
var rows = JsonUtils.loadToMap(dropPath, String.class, DropRow.class);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
var aggregate = new LinkedHashMap<Integer, List<Integer>>();
|
||||
for (var row : rows.values()) {
|
||||
if (row == null || row.DropId <= 0 || row.PkgId <= 0) {
|
||||
continue;
|
||||
}
|
||||
aggregate.computeIfAbsent(row.DropId, ignored -> new ArrayList<>()).add(row.PkgId);
|
||||
}
|
||||
|
||||
for (var entry : aggregate.entrySet()) {
|
||||
var values = entry.getValue();
|
||||
if (values == null || values.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int[] packageIds = new int[values.size()];
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
packageIds[i] = values.get(i);
|
||||
}
|
||||
result.put(entry.getKey(), packageIds);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Nebula.getLogger().error("Failed to load battle-pass drop mapping from {}", dropPath, exception);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal Drop.json row shape needed for battle-pass mapping.
|
||||
*/
|
||||
private static final class DropRow {
|
||||
private int DropId;
|
||||
private int PkgId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Battle-pass collect triggers 3 client presentations:
|
||||
* skin animation (notify queue), normal reward popup (CollectResp.Items), level refresh (main response).
|
||||
* <p>
|
||||
* Only popup-safe items are shown in reward popup to avoid duplicate skin/head display.
|
||||
* Avatar rewards are converted to item format for the client's general reward popup.
|
||||
*/
|
||||
private ChangeInfo buildBattlePassDisplayChange(PlayerChangeInfo change) {
|
||||
if (change == null || change.isEmpty()) {
|
||||
return ChangeInfo.newInstance();
|
||||
}
|
||||
|
||||
var display = new PlayerChangeInfo();
|
||||
for (var any : change.getList()) {
|
||||
String typeUrl = any.getTypeUrl();
|
||||
if (typeUrl.endsWith(Item.class.getSimpleName()) || typeUrl.endsWith(Res.class.getSimpleName())) {
|
||||
display.getList().add(any.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeUrl.endsWith(HeadIcon.class.getSimpleName())) {
|
||||
try {
|
||||
var headIcon = HeadIcon.parseFrom(any.getValue().toArray());
|
||||
display.add(Item.newInstance().setTid(headIcon.getTid()).setQty(1));
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return display.toProto();
|
||||
}
|
||||
|
||||
private OrderInfo createOrder(GameSession session, String orderType, String payloadKey, CollectSettlement settlement) {
|
||||
int uid = session.getPlayer().getUid();
|
||||
String orderId = orderType + "." + uid + "." + System.currentTimeMillis();
|
||||
this.pendingCollects.put(uid, settlement);
|
||||
return this.buildMockOrder(orderId, orderType + ":" + payloadKey + ":" + orderId);
|
||||
}
|
||||
|
||||
private OrderInfo buildMockOrder(String orderId, String extraData) {
|
||||
var paidNotify = OrderStateChange.newInstance()
|
||||
.setOrderId(orderId)
|
||||
.setStore(orderId.startsWith("battlepass.") ? 3 : 1);
|
||||
|
||||
return OrderInfo.newInstance()
|
||||
.setId(orderId)
|
||||
.setExtraData(extraData)
|
||||
.setNotifyUrl(String.format("http://localhost:%s/mock-pay", Nebula.getConfig().getHttpServer().getBindPort()))
|
||||
.setNextPackage(PacketHelper.encodeMsg(NetMsgId.order_paid_notify, paidNotify));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package emu.nebula.game.player;
|
||||
|
||||
import java.util.Stack;
|
||||
|
||||
import dev.morphia.annotations.AlsoLoad;
|
||||
import dev.morphia.annotations.Entity;
|
||||
import dev.morphia.annotations.Id;
|
||||
import dev.morphia.annotations.Indexed;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.data.resources.MallMonthlyCardDef;
|
||||
import emu.nebula.database.GameDatabaseObject;
|
||||
import emu.nebula.game.account.Account;
|
||||
import emu.nebula.game.achievement.AchievementCondition;
|
||||
@@ -35,27 +33,23 @@ import emu.nebula.game.vampire.VampireSurvivorManager;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.net.NetMsgPacket;
|
||||
import emu.nebula.proto.Notify.MonthlyCardRewards;
|
||||
import emu.nebula.proto.Notify.SigninRewardUpdate;
|
||||
import emu.nebula.proto.PlayerData.DictionaryEntry;
|
||||
import emu.nebula.proto.PlayerData.DictionaryTab;
|
||||
import emu.nebula.proto.PlayerData.PlayerInfo;
|
||||
import emu.nebula.proto.Public.CharShow;
|
||||
import emu.nebula.proto.Public.Energy;
|
||||
import emu.nebula.proto.Public.Friend;
|
||||
import emu.nebula.proto.Public.HonorInfo;
|
||||
import emu.nebula.proto.Public.Item;
|
||||
import emu.nebula.proto.Public.NewbieInfo;
|
||||
import emu.nebula.proto.Public.QuestType;
|
||||
import emu.nebula.proto.Public.Res;
|
||||
import emu.nebula.proto.Public.WorldClass;
|
||||
import emu.nebula.proto.Public.WorldClassRewardState;
|
||||
import emu.nebula.proto.Public;
|
||||
import emu.nebula.proto.Public.*;
|
||||
import emu.nebula.util.ResetCycle;
|
||||
import emu.nebula.util.Utils;
|
||||
import emu.nebula.proto.Public.Title;
|
||||
|
||||
import emu.nebula.util.ints.String2IntMap;
|
||||
import lombok.Getter;
|
||||
import us.hebi.quickbuf.ProtoMessage;
|
||||
import us.hebi.quickbuf.RepeatedInt;
|
||||
|
||||
import java.lang.String;
|
||||
import java.util.Stack;
|
||||
|
||||
@Getter
|
||||
@Entity(value = "players", useDiscriminator = false)
|
||||
public class Player implements GameDatabaseObject {
|
||||
@@ -92,6 +86,8 @@ public class Player implements GameDatabaseObject {
|
||||
private long lastEpochDay;
|
||||
private long lastLogin;
|
||||
private long createTime;
|
||||
private String2IntMap monthlyCardExpireDays;
|
||||
private String2IntMap monthlyCardLastRewardDays;
|
||||
|
||||
// Managers
|
||||
private final transient CharacterStorage characters;
|
||||
@@ -165,6 +161,8 @@ public class Player implements GameDatabaseObject {
|
||||
this.level = 1;
|
||||
this.energy = 240;
|
||||
this.energyLastUpdate = this.createTime;
|
||||
this.monthlyCardExpireDays = new String2IntMap();
|
||||
this.monthlyCardLastRewardDays = new String2IntMap();
|
||||
|
||||
// Setup inventory
|
||||
this.inventory = new Inventory(this);
|
||||
@@ -217,6 +215,8 @@ public class Player implements GameDatabaseObject {
|
||||
}
|
||||
|
||||
public void setLevel(int level) {
|
||||
int oldLevel = this.level;
|
||||
|
||||
// Set player world class (level)
|
||||
this.level = level;
|
||||
|
||||
@@ -225,6 +225,10 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Trigger achievement
|
||||
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
|
||||
|
||||
if (oldLevel != this.level) {
|
||||
this.queueBattlePassUnlockNotify(oldLevel);
|
||||
}
|
||||
}
|
||||
|
||||
public void setExp(int exp) {
|
||||
@@ -547,6 +551,8 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Trigger achievement
|
||||
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
|
||||
|
||||
this.queueBattlePassUnlockNotify(oldLevel);
|
||||
}
|
||||
|
||||
// Calculate changes
|
||||
@@ -555,7 +561,7 @@ public class Player implements GameDatabaseObject {
|
||||
.setExpChange(this.getExp() - oldExp);
|
||||
|
||||
changes.add(proto);
|
||||
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
@@ -637,26 +643,29 @@ public class Player implements GameDatabaseObject {
|
||||
// Dailies
|
||||
|
||||
public void checkResetDailies() {
|
||||
// Sanity check to make sure daily reset isnt being triggered wrong
|
||||
if (Nebula.getGameContext().getEpochDays() <= this.getLastEpochDay()) {
|
||||
long currentResetDay = Utils.getResetEpochDay();
|
||||
|
||||
// Sanity check to make sure daily reset isn't being triggered wrong
|
||||
if (currentResetDay <= this.getLastEpochDay()) {
|
||||
// Fix sign-in index
|
||||
// TODO remove later
|
||||
if (this.getSignInIndex() <= 0) {
|
||||
this.getSignInRewards(false);
|
||||
}
|
||||
|
||||
this.refreshMonthlyCardRewards(false);
|
||||
|
||||
// End
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if week has changed (Resets on monday)
|
||||
// TODO add a config option
|
||||
int curWeek = Utils.getWeeks(this.getLastEpochDay());
|
||||
boolean hasWeekChanged = Nebula.getGameContext().getEpochWeeks() > curWeek;
|
||||
// Check if week has changed (Resets on Monday)
|
||||
int curWeek = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, this.getLastResetDayTimeSeconds());
|
||||
boolean hasWeekChanged = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, Nebula.getCurrentServerTime()) > curWeek;
|
||||
|
||||
// Check if month was changed
|
||||
int curMonth = Utils.getMonths(this.getLastEpochDay());
|
||||
boolean hasMonthChanged = Nebula.getGameContext().getEpochMonths() > curMonth;
|
||||
int curMonth = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, this.getLastResetDayTimeSeconds());
|
||||
boolean hasMonthChanged = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, Nebula.getCurrentServerTime()) > curMonth;
|
||||
|
||||
// Reset dailies
|
||||
this.resetDailies(hasWeekChanged, hasMonthChanged);
|
||||
@@ -666,9 +675,10 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Give sign-in rewards
|
||||
this.getSignInRewards(hasMonthChanged);
|
||||
this.refreshMonthlyCardRewards(true);
|
||||
|
||||
// Update last epoch day
|
||||
this.lastEpochDay = Nebula.getGameContext().getEpochDays();
|
||||
this.lastEpochDay = currentResetDay;
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), "lastEpochDay", this.lastEpochDay);
|
||||
}
|
||||
|
||||
@@ -680,7 +690,7 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Get next sign-in index
|
||||
int nextSignIn = this.signInIndex + 1;
|
||||
int group = Utils.getDaysOfMonth(this.getLastEpochDay());
|
||||
int group = Utils.getDaysOfMonth(Utils.getResetEpochDay());
|
||||
|
||||
var data = GameData.getSignInDataTable().get((group << 16) + nextSignIn);
|
||||
if (data == null) {
|
||||
@@ -725,12 +735,14 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Reset weekly tower tickets
|
||||
this.getProgress().clearWeeklyTowerTicketLog();
|
||||
this.getInventory().resetWeeklyMallPackagePurchases();
|
||||
}
|
||||
|
||||
// Check if we need to reset monthly
|
||||
if (resetMonthly) {
|
||||
// Reset monthly shop purchases
|
||||
this.getInventory().resetShopPurchases();
|
||||
this.getInventory().resetMonthlyMallPackagePurchases();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,6 +821,16 @@ public class Player implements GameDatabaseObject {
|
||||
this.showChars = new int[3];
|
||||
this.save();
|
||||
}
|
||||
|
||||
if (this.monthlyCardExpireDays == null) {
|
||||
this.monthlyCardExpireDays = new String2IntMap();
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), "monthlyCardExpireDays", this.monthlyCardExpireDays);
|
||||
}
|
||||
|
||||
if (this.monthlyCardLastRewardDays == null) {
|
||||
this.monthlyCardLastRewardDays = new String2IntMap();
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), "monthlyCardLastRewardDays", this.monthlyCardLastRewardDays);
|
||||
}
|
||||
|
||||
// Init activities
|
||||
this.getActivityManager().init();
|
||||
@@ -829,13 +851,225 @@ public class Player implements GameDatabaseObject {
|
||||
// Fix any broken honor ids
|
||||
this.checkBrokenHonor();
|
||||
|
||||
// Update activities
|
||||
this.getActivityManager().onLogin();
|
||||
|
||||
// Update last login time
|
||||
this.lastLogin = System.currentTimeMillis();
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), "lastLogin", this.getLastLogin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the player currently has at least one purchasable
|
||||
* free mall package (`CurrencyType = 3`) for the mall entrance red dot.
|
||||
*/
|
||||
public boolean hasAvailableFreeMallPackage() {
|
||||
for (var data : GameData.getMallPackageDataTable()) {
|
||||
if (!data.isFreePackage()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data.canPurchase(this)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the mall entrance red dot state for free mall packages (`CurrencyType = 3`).
|
||||
* Specific package entries are still refreshed from the package list itself.
|
||||
*/
|
||||
public void queueMallPackageStateNotify() {
|
||||
this.addNextPackage(
|
||||
NetMsgId.mall_package_state_notify,
|
||||
MallPackageState.newInstance().setNew(this.hasAvailableFreeMallPackage())
|
||||
);
|
||||
}
|
||||
|
||||
public void queueBattlePassStateNotify() {
|
||||
this.addNextPackage(
|
||||
NetMsgId.battle_pass_state_notify,
|
||||
this.buildBattlePassStateProto()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a full battle-pass snapshot for clients that only refresh their
|
||||
* local battle-pass cache/red dots from the page-info payload.
|
||||
*/
|
||||
public void queueBattlePassInfoNotify() {
|
||||
this.addNextPackage(
|
||||
NetMsgId.battle_pass_info_succeed_ack,
|
||||
this.getBattlePassManager().getBattlePass().toProto()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Battle-pass unlock is a special case: clients need the full page snapshot
|
||||
* to initialize local battle-pass cache and refresh the main-entry red dot.
|
||||
*/
|
||||
private void queueBattlePassUnlockNotify(int oldLevel) {
|
||||
if (oldLevel < GameConstants.BATTLE_PASS_UNLOCK_LEVEL && this.isBattlePassUnlocked()) {
|
||||
this.queueBattlePassInfoNotify();
|
||||
}
|
||||
}
|
||||
|
||||
private Public.BattlePassState buildBattlePassStateProto() {
|
||||
int state = this.isBattlePassUnlocked()
|
||||
? this.getBattlePassManager().getBattlePass().getClientState()
|
||||
: 0;
|
||||
|
||||
return Public.BattlePassState.newInstance().setState(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of future game days still covered after the current game day.
|
||||
* On the last claimable day this value is 0 even though today's reward can still be claimed.
|
||||
*/
|
||||
public int getMonthlyCardRemainingDays(String cardId) {
|
||||
if (this.monthlyCardExpireDays == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int endDay = this.monthlyCardExpireDays.get(cardId);
|
||||
long currentDay = Utils.getResetEpochDay();
|
||||
|
||||
if (endDay < currentDay) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) (endDay - currentDay);
|
||||
}
|
||||
|
||||
public boolean receivedMonthlyCardRewardToday(String cardId) {
|
||||
return cardId != null
|
||||
&& this.monthlyCardLastRewardDays != null
|
||||
&& this.monthlyCardLastRewardDays.get(cardId) >= Utils.getResetEpochDay();
|
||||
}
|
||||
|
||||
public long getMonthlyCardEndTime(String cardId) {
|
||||
if (cardId == null || this.monthlyCardExpireDays == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int endDay = this.monthlyCardExpireDays.get(cardId);
|
||||
if (endDay <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Utils.getResetTimeSecondsByEpochDay((long) endDay + 1);
|
||||
}
|
||||
|
||||
private void setMonthlyCardRewardEndDay(String cardId, int endDay) {
|
||||
if (this.monthlyCardExpireDays == null) {
|
||||
this.monthlyCardExpireDays = new String2IntMap();
|
||||
}
|
||||
|
||||
this.monthlyCardExpireDays.put(cardId, endDay);
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), "monthlyCardExpireDays", this.monthlyCardExpireDays);
|
||||
}
|
||||
|
||||
private void setMonthlyCardLastRewardDay(String cardId, int rewardDay) {
|
||||
if (this.monthlyCardLastRewardDays == null) {
|
||||
this.monthlyCardLastRewardDays = new String2IntMap();
|
||||
}
|
||||
|
||||
this.monthlyCardLastRewardDays.put(cardId, rewardDay);
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), "monthlyCardLastRewardDays", this.monthlyCardLastRewardDays);
|
||||
}
|
||||
|
||||
public void activateMonthlyCard(String cardId, int durationDays) {
|
||||
if (cardId == null || durationDays <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int currentDay = Math.toIntExact(Utils.getResetEpochDay());
|
||||
int remainingDays = this.getMonthlyCardRemainingDays(cardId);
|
||||
boolean claimedToday = this.receivedMonthlyCardRewardToday(cardId);
|
||||
|
||||
// remainingDays:
|
||||
// - > 0: directly add durationDays
|
||||
// - == 0 and claimed today: add full durationDays
|
||||
// - == 0 and not claimed today: purchase auto-claims today's reward, so keep durationDays - 1 future days
|
||||
int newRemainingDays = remainingDays > 0 || claimedToday
|
||||
? remainingDays + durationDays
|
||||
: Math.max(durationDays - 1, 0);
|
||||
|
||||
this.setMonthlyCardRewardEndDay(cardId, currentDay + newRemainingDays);
|
||||
}
|
||||
|
||||
public PlayerChangeInfo createMonthlyCardRewardChange(String cardId) {
|
||||
var cardData = this.getMonthlyCardData(cardId);
|
||||
if (cardData == null || !this.canClaimMonthlyCardReward(cardId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var rewards = cardData.getDailyRewards();
|
||||
if (rewards.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var change = this.getInventory().addItems(rewards);
|
||||
this.setMonthlyCardLastRewardDay(cardId, Math.toIntExact(Utils.getResetEpochDay()));
|
||||
return change;
|
||||
}
|
||||
|
||||
public void grantMonthlyCardReward(String cardId, boolean notifyOnly) {
|
||||
var cardData = this.getMonthlyCardData(cardId);
|
||||
if (cardData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var change = this.createMonthlyCardRewardChange(cardId);
|
||||
if (change == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rewards = cardData.getDailyRewards();
|
||||
|
||||
var notify = MonthlyCardRewards.newInstance()
|
||||
.setId(cardData.getMonthlyCardId())
|
||||
.setRemaining(this.getMonthlyCardRemainingDays(cardId))
|
||||
.setSwitch(!notifyOnly)
|
||||
.setEndTime(this.getMonthlyCardEndTime(cardId));
|
||||
|
||||
var changeProto = change.toProto();
|
||||
notify.setChange(changeProto);
|
||||
rewards.toItemTemplateStream().forEach(notify::addRewards);
|
||||
|
||||
this.addNextPackage(NetMsgId.monthly_card_rewards_notify, notify);
|
||||
|
||||
if (!changeProto.isEmpty()) {
|
||||
this.addNextPackage(NetMsgId.items_change_notify, changeProto);
|
||||
}
|
||||
}
|
||||
|
||||
private MallMonthlyCardDef getMonthlyCardData(String cardId) {
|
||||
return cardId == null ? null : GameData.getMallMonthlyCardDataTable().get(cardId.hashCode());
|
||||
}
|
||||
|
||||
private boolean canClaimMonthlyCardReward(String cardId) {
|
||||
if (this.monthlyCardExpireDays == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int today = Math.toIntExact(Utils.getResetEpochDay());
|
||||
int endDay = this.monthlyCardExpireDays.get(cardId);
|
||||
return endDay > 0 && today <= endDay && !this.receivedMonthlyCardRewardToday(cardId);
|
||||
}
|
||||
|
||||
private void refreshMonthlyCardRewards(boolean notifyOnly) {
|
||||
if (this.monthlyCardExpireDays == null || this.monthlyCardExpireDays.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var entry : this.monthlyCardExpireDays.object2IntEntrySet()) {
|
||||
this.grantMonthlyCardReward(entry.getKey(), notifyOnly);
|
||||
}
|
||||
}
|
||||
|
||||
private long getLastResetDayTimeSeconds() {
|
||||
return Utils.getResetTimeSecondsByEpochDay(this.getLastEpochDay());
|
||||
}
|
||||
|
||||
// Next packages
|
||||
|
||||
@@ -846,7 +1080,7 @@ public class Player implements GameDatabaseObject {
|
||||
public void addNextPackage(int msgId, ProtoMessage<?> proto) {
|
||||
this.getNextPackages().add(new NetMsgPacket(msgId, proto));
|
||||
}
|
||||
|
||||
|
||||
// Misc
|
||||
|
||||
/**
|
||||
@@ -984,8 +1218,9 @@ public class Player implements GameDatabaseObject {
|
||||
state.getMutableMail()
|
||||
.setNew(this.getMailbox().hasNewMail());
|
||||
|
||||
state.getMutableBattlePass()
|
||||
.setState(this.getBattlePassManager().hasNew() ? 1 : 0);
|
||||
// Keep BattlePass state container present for client-side login cache flow.
|
||||
// Some clients assume State.BattlePass exists during player_data bootstrap.
|
||||
state.setBattlePass(this.buildBattlePassStateProto());
|
||||
|
||||
state.getMutableAchievement()
|
||||
.setNew(this.getAchievementManager().hasNewAchievements());
|
||||
@@ -1073,7 +1308,7 @@ public class Player implements GameDatabaseObject {
|
||||
// Complete
|
||||
return proto;
|
||||
}
|
||||
|
||||
|
||||
public Friend getFriendProto() {
|
||||
var proto = Friend.newInstance()
|
||||
.setId(this.getUid())
|
||||
@@ -1115,5 +1350,9 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean isBattlePassUnlocked() {
|
||||
return this.getLevel() >= GameConstants.BATTLE_PASS_UNLOCK_LEVEL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package emu.nebula.game.player;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public abstract class PlayerManager {
|
||||
private transient Player player;
|
||||
|
||||
@@ -11,10 +14,6 @@ public abstract class PlayerManager {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
public void setPlayer(Player player) {
|
||||
if (this.player == null) {
|
||||
this.player = player;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package emu.nebula.game.quest;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import emu.nebula.util.Utils;
|
||||
import emu.nebula.util.ResetCycle;
|
||||
import emu.nebula.proto.Public.Quest;
|
||||
import emu.nebula.proto.Public.QuestProgress;
|
||||
import lombok.Getter;
|
||||
@@ -103,7 +105,21 @@ public class GameQuest {
|
||||
.setTypeValue(this.getType())
|
||||
.setStatus(this.getStatus())
|
||||
.addProgress(progress);
|
||||
|
||||
long expire = this.getExpireTime();
|
||||
if (expire > 0) {
|
||||
proto.setExpire(expire);
|
||||
}
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
private long getExpireTime() {
|
||||
return switch (this.type) {
|
||||
case QuestType.BattlePassDaily -> Utils.getNextResetTimeSeconds(ResetCycle.DAILY);
|
||||
case QuestType.BattlePassWeekly -> Utils.getNextResetTimeSeconds(ResetCycle.WEEKLY);
|
||||
default -> 0L;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -148,11 +148,12 @@ public enum QuestCondition {
|
||||
}
|
||||
}
|
||||
|
||||
private QuestCondition(int value) {
|
||||
QuestCondition(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static QuestCondition getByValue(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user