Implement new weekly quests

This commit is contained in:
Melledy
2025-12-24 00:03:05 -08:00
parent c2ebff92c9
commit 9df83c5205
15 changed files with 437 additions and 46 deletions
+3 -1
View File
@@ -97,9 +97,11 @@ public class GameData {
@Getter private static DataTable<MainScreenCGDef> MainScreenCGDataTable = new DataTable<>(); @Getter private static DataTable<MainScreenCGDef> MainScreenCGDataTable = new DataTable<>();
// ===== Daily Quests ===== // ===== Daily/Weekly Quests =====
@Getter private static DataTable<DailyQuestDef> DailyQuestDataTable = new DataTable<>(); @Getter private static DataTable<DailyQuestDef> DailyQuestDataTable = new DataTable<>();
@Getter private static DataTable<DailyQuestActiveDef> DailyQuestActiveDataTable = new DataTable<>(); @Getter private static DataTable<DailyQuestActiveDef> DailyQuestActiveDataTable = new DataTable<>();
@Getter private static DataTable<WeeklyQuestDef> WeeklyQuestDataTable = new DataTable<>();
@Getter private static DataTable<WeeklyQuestActiveDef> WeeklyQuestActiveDataTable = new DataTable<>();
// ===== Achievements ===== // ===== Achievements =====
@Getter private static DataTable<AchievementDef> AchievementDataTable = new DataTable<>(); @Getter private static DataTable<AchievementDef> AchievementDataTable = new DataTable<>();
@@ -23,6 +23,11 @@ public class BattlePassQuestDef extends BaseDef implements QuestData {
return Id; return Id;
} }
@Override
public long getQuestKey() {
return Id;
}
public boolean isDaily() { public boolean isDaily() {
return this.Type == 1; return this.Type == 1;
} }
@@ -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 = "WeeklyQuestActive.json")
public class WeeklyQuestActiveDef extends BaseDef {
private int Id;
private int Active;
private int ItemTid1;
private int Number1;
private int ItemTid2;
private int Number2;
private transient ItemParamMap rewards;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.rewards = new ItemParamMap();
if (this.ItemTid1 > 0 && this.Number1 > 0) {
this.rewards.add(this.ItemTid1, this.Number1);
}
if (this.ItemTid2 > 0 && this.Number2 > 0) {
this.rewards.add(this.ItemTid2, this.Number2);
}
}
}
@@ -0,0 +1,49 @@
package emu.nebula.data.resources;
import java.util.Arrays;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.quest.QuestData;
import emu.nebula.game.quest.QuestType;
import lombok.Getter;
@Getter
@ResourceType(name = "WeeklyQuest.json")
public class WeeklyQuestDef extends BaseDef implements QuestData {
private int Id;
private boolean Apear;
private int Active;
private int ItemTid;
private int ItemQty;
private int CompleteCond;
private int CompleteCondClient;
private String CompleteCondParams;
private transient int[] condParams;
@Override
public int getId() {
return Id;
}
@Override
public int getQuestType() {
return QuestType.Weekly;
}
public int[] getCompleteCondParams() {
return this.condParams;
}
@Override
public void onLoad() {
this.condParams = Arrays.stream(this.CompleteCondParams.split("[\\[,\\]]"))
.filter(s -> !s.isEmpty())
.mapToInt(Integer::parseInt)
.toArray();
this.CompleteCondParams = null;
}
}
@@ -687,7 +687,7 @@ public class Player implements GameDatabaseObject {
public void resetDailies(boolean resetWeekly, boolean resetMonthly) { public void resetDailies(boolean resetWeekly, boolean resetMonthly) {
// Reset daily quests // Reset daily quests
this.getQuestManager().resetDailyQuests(); this.getQuestManager().resetDailyQuests(resetWeekly);
this.getBattlePassManager().getBattlePass().resetDailyQuests(resetWeekly); this.getBattlePassManager().getBattlePass().resetDailyQuests(resetWeekly);
// Check to reset weeklies // Check to reset weeklies
@@ -740,6 +740,7 @@ public class Player implements GameDatabaseObject {
if (manager != null) { if (manager != null) {
manager.setPlayer(this); manager.setPlayer(this);
manager.onLoad();
} else { } else {
try { try {
manager = cls.getDeclaredConstructor(Player.class).newInstance(this); manager = cls.getDeclaredConstructor(Player.class).newInstance(this);
@@ -24,4 +24,11 @@ public abstract class PlayerManager {
public int getPlayerUid() { public int getPlayerUid() {
return this.getPlayer().getUid(); return this.getPlayer().getUid();
} }
/**
* Called when the manager is loaded from the database
*/
public void onLoad() {
}
} }
@@ -36,6 +36,10 @@ public class GameQuest {
} }
} }
public long getKey() {
return ((long) this.getType() << 32) + this.getId();
}
public void resetProgress() { public void resetProgress() {
this.curProgress = 0; this.curProgress = 0;
this.claimed = false; this.claimed = false;
@@ -45,6 +49,10 @@ public class GameQuest {
return this.curProgress >= this.maxProgress; return this.curProgress >= this.maxProgress;
} }
public boolean canClaim() {
return this.isComplete() && !this.isClaimed();
}
private int getStatus() { private int getStatus() {
if (this.isClaimed()) { if (this.isClaimed()) {
return 2; return 2;
@@ -2,8 +2,18 @@ package emu.nebula.game.quest;
public interface QuestData { public interface QuestData {
/**
* Quest id, used by the client
*/
public int getId(); public int getId();
/**
* This is the computed key for the quest in hashmaps
*/
public default long getQuestKey() {
return ((long) this.getQuestType() << 32) + this.getId();
}
public int getQuestType(); public int getQuestType();
public int getCompleteCond(); public int getCompleteCond();
@@ -1,6 +1,7 @@
package emu.nebula.game.quest; package emu.nebula.game.quest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -34,9 +35,10 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
// Daily activity missions // Daily activity missions
private IntSet claimedActiveIds; private IntSet claimedActiveIds;
private IntSet claimedWeeklyIds;
// Quests // Quests
private Map<Integer, GameQuest> quests; private Map<Long, GameQuest> list;
// Level rewards // Level rewards
private Bitset levelRewards; private Bitset levelRewards;
@@ -53,15 +55,30 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
super(player); super(player);
this.uid = player.getUid(); this.uid = player.getUid();
this.claimedActiveIds = new IntOpenHashSet(); this.claimedActiveIds = new IntOpenHashSet();
this.quests = new HashMap<>(); this.claimedWeeklyIds = new IntOpenHashSet();
this.list = new HashMap<>();
this.levelRewards = new Bitset(); this.levelRewards = new Bitset();
this.hasDailyReward = true; this.hasDailyReward = true;
this.resetDailyQuests(); this.resetDailyQuests(true);
this.save(); this.save();
} }
public synchronized GameQuest getQuest(int type, int questId) {
long key = ((long) type << 32) + questId;
return this.list.get(key);
}
public synchronized GameQuest addQuest(GameQuest quest) {
this.list.put(quest.getKey(), quest);
return quest;
}
public synchronized Collection<GameQuest> getQuests() {
return this.getList().values();
}
public boolean hasDailyReward() { public boolean hasDailyReward() {
return this.hasDailyReward; return this.hasDailyReward;
} }
@@ -70,34 +87,15 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
Nebula.getGameDatabase().update(this, this.getUid(), "levelRewards", this.levelRewards); Nebula.getGameDatabase().update(this, this.getUid(), "levelRewards", this.levelRewards);
} }
public synchronized int getActivity() { public synchronized void resetDailyQuests(boolean resetWeekly) {
int activity = 0;
for (var quest : getQuests().values()) {
if (quest.getType() != QuestType.Daily) {
continue;
}
if (!quest.isClaimed()) {
continue;
}
var data = GameData.getDailyQuestDataTable().get(quest.getId());
if (data == null) {
continue;
}
activity += data.getActive();
}
return activity;
}
public synchronized void resetDailyQuests() {
// Reset daily quests // Reset daily quests
for (var data : GameData.getDailyQuestDataTable()) { for (var data : GameData.getDailyQuestDataTable()) {
// Get quest // Get quest
var quest = getQuests().computeIfAbsent(data.getId(), i -> new GameQuest(data)); var quest = getList().get(data.getQuestKey());
if (quest == null) {
quest = this.addQuest(new GameQuest(data));
}
// Reset progress // Reset progress
quest.resetProgress(); quest.resetProgress();
@@ -106,8 +104,31 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
this.syncQuest(quest); this.syncQuest(quest);
} }
// Reset weekly quests
if (resetWeekly) {
for (var data : GameData.getWeeklyQuestDataTable()) {
// Get quest
var quest = getList().get(data.getQuestKey());
if (quest == null) {
quest = this.addQuest(new GameQuest(data));
}
// Reset progress
quest.resetProgress();
// Sync quest with player client
this.syncQuest(quest);
}
// Reset weekly ids
this.claimedWeeklyIds.clear();
}
// Reset activity // Reset activity
this.claimedActiveIds.clear(); this.claimedActiveIds.clear();
// Reset daily shop reward
this.hasDailyReward = true; this.hasDailyReward = true;
// Persist to database // Persist to database
@@ -115,7 +136,11 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
} }
public synchronized void trigger(int condition, int progress, int param1, int param2) { public synchronized void trigger(int condition, int progress, int param1, int param2) {
for (var quest : getQuests().values()) { // TODO optimize so we dont loop through every quest
for (var entry : getList().entrySet()) {
// Get quest from entry
var quest = entry.getValue();
// Try to trigger quest // Try to trigger quest
boolean result = quest.trigger(condition, progress, param1, param2); boolean result = quest.trigger(condition, progress, param1, param2);
@@ -128,7 +153,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
this.syncQuest(quest); this.syncQuest(quest);
// Update in database // Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "quests." + quest.getId(), quest); Nebula.getGameDatabase().update(this, this.getUid(), "list." + entry.getKey(), quest);
} }
} }
@@ -146,21 +171,48 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
); );
} }
public PlayerChangeInfo receiveQuestReward(int questId) { // Daily quests
public synchronized int getDailyActivity() {
int activity = 0;
for (var quest : this.getQuests()) {
if (quest.getType() != QuestType.Daily) {
continue;
}
if (!quest.isClaimed()) {
continue;
}
var data = GameData.getDailyQuestDataTable().get(quest.getId());
if (data == null) continue;
activity += data.getActive();
}
return activity;
}
public PlayerChangeInfo receiveDailyQuestReward(int questId) {
// Get received quests // Get received quests
var claimList = new ArrayList<GameQuest>(); var claimList = new ArrayList<GameQuest>();
if (questId > 0) { if (questId > 0) {
// Claim specific quest // Claim specific quest
var quest = this.getQuests().get(questId); var quest = this.getQuest(QuestType.Daily, questId);
if (quest != null && !quest.isClaimed()) { if (quest != null && !quest.isClaimed()) {
claimList.add(quest); claimList.add(quest);
} }
} else { } else {
// Claim all // Claim all
for (var quest : this.getQuests().values()) { for (var quest : this.getQuests()) {
if (!quest.isComplete() || quest.isClaimed()) { if (quest.getType() != QuestType.Daily) {
continue;
}
if (!quest.canClaim()) {
continue; continue;
} }
@@ -189,7 +241,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
quest.setClaimed(true); quest.setClaimed(true);
// Update in database // Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "quests." + quest.getId(), quest); Nebula.getGameDatabase().update(this, this.getUid(), "list." + quest.getKey(), quest);
} }
// Trigger quest // Trigger quest
@@ -199,12 +251,12 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
return change.setSuccess(true); return change.setSuccess(true);
} }
public PlayerChangeInfo claimActiveRewards() { public PlayerChangeInfo claimDailyActiveRewards() {
// Init // Init
var claimList = new IntArrayList(); var claimList = new IntArrayList();
var rewards = new ItemParamMap(); var rewards = new ItemParamMap();
int activity = this.getActivity(); int activity = this.getDailyActivity();
// Get claimable // Get claimable
for (var data : GameData.getDailyQuestActiveDataTable()) { for (var data : GameData.getDailyQuestActiveDataTable()) {
@@ -240,6 +292,129 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
return change.setSuccess(true); return change.setSuccess(true);
} }
// Weekly quests
public synchronized int getWeeklyActivity() {
int activity = 0;
for (var quest : getQuests()) {
if (quest.getType() != QuestType.Weekly) {
continue;
}
if (!quest.isClaimed()) {
continue;
}
var data = GameData.getWeeklyQuestDataTable().get(quest.getId());
if (data == null) continue;
activity += data.getActive();
}
return activity;
}
public PlayerChangeInfo receiveWeeklyQuestReward(int questId) {
// Get received quests
var claimList = new ArrayList<GameQuest>();
if (questId > 0) {
// Claim specific quest
var quest = this.getQuest(QuestType.Weekly, questId);
if (quest != null && !quest.isClaimed()) {
claimList.add(quest);
}
} else {
// Claim all
for (var quest : this.getQuests()) {
if (quest.getType() != QuestType.Weekly) {
continue;
}
if (!quest.canClaim()) {
continue;
}
claimList.add(quest);
}
}
// Sanity check
if (claimList.isEmpty()) {
return null;
}
// Create change info
var change = new PlayerChangeInfo();
// Claim
for (var quest : claimList) {
// Get data
var data = GameData.getWeeklyQuestDataTable().get(quest.getId());
if (data != null) {
// Add reward data
this.getPlayer().getInventory().addItem(data.getItemTid(), data.getItemQty(), change);
}
// Set claimed
quest.setClaimed(true);
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "list." + quest.getKey(), quest);
}
// Trigger quest
this.getPlayer().trigger(QuestCondition.QuestWithSpecificType, claimList.size(), QuestType.Weekly);
// Success
return change.setSuccess(true);
}
public PlayerChangeInfo claimWeeklyActiveRewards() {
// Init
var claimList = new IntArrayList();
var rewards = new ItemParamMap();
int activity = this.getWeeklyActivity();
// Get claimable
for (var data : GameData.getWeeklyQuestActiveDataTable()) {
if (this.getClaimedWeeklyIds().contains(data.getId())) {
continue;
}
if (activity >= data.getActive()) {
// Add rewards
rewards.add(data.getRewards());
// Add to claimed activity list
claimList.add(data.getId());
}
}
// Sanity check
if (claimList.isEmpty()) {
return null;
}
// Add rewards
var change = this.getPlayer().getInventory().addItems(rewards);
// Set claimed list
change.setExtraData(claimList);
// Update in database
this.getClaimedWeeklyIds().addAll(claimList);
Nebula.getGameDatabase().update(this, this.getUid(), "claimedWeeklyIds", this.getClaimedWeeklyIds());
// Success
return change.setSuccess(true);
}
// World level rewards
public PlayerChangeInfo receiveWorldClassReward(int id) { public PlayerChangeInfo receiveWorldClassReward(int id) {
// Get rewards we want to claim // Get rewards we want to claim
var claimList = new ArrayList<WorldClassDef>(); var claimList = new ArrayList<WorldClassDef>();
@@ -287,9 +462,9 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
return change.setSuccess(true); return change.setSuccess(true);
} }
// Daily reward // Daily shop reward
public PlayerChangeInfo claimDailyReward() { public PlayerChangeInfo claimDailyShopGift() {
// Sanity check // Sanity check
if (!this.hasDailyReward) { if (!this.hasDailyReward) {
return null; return null;
@@ -310,12 +485,40 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
return change.setSuccess(true); return change.setSuccess(true);
} }
// Database
@Override
public void onLoad() {
boolean shouldSave = false;
// Fix missing weekly quests
if (this.claimedWeeklyIds == null) {
this.claimedWeeklyIds = new IntOpenHashSet();
shouldSave = true;
}
// Fix quest lists
if (this.getList() == null) {
this.list = new HashMap<>();
this.resetDailyQuests(true);
// QuestManager::resetDailyQuests will save to the database, so we dont need to set the shouldSave flag
shouldSave = false;
}
// Save to database
if (shouldSave) {
this.save();
}
}
// Serialization // Serialization
public void encodePlayerInfo(PlayerInfo proto) { public void encodePlayerInfo(PlayerInfo proto) {
// Quests proto
var quests = proto.getMutableQuests(); var quests = proto.getMutableQuests();
for (var quest : this.getQuests().values()) { for (var quest : this.getQuests()) {
quests.addList(quest.toProto()); quests.addList(quest.toProto());
} }
@@ -324,6 +527,10 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
proto.addDailyActiveIds(id); proto.addDailyActiveIds(id);
} }
for (int id : this.getClaimedWeeklyIds()) {
proto.addWeeklyActiveIds(id);
}
// Set world level rewards // Set world level rewards
proto.getMutableState() proto.getMutableState()
.getMutableWorldClassReward() .getMutableWorldClassReward()
@@ -1,5 +1,6 @@
package emu.nebula.game.quest; package emu.nebula.game.quest;
// From public.proto
public class QuestType { public class QuestType {
public static final int UnknownQuest = 0; public static final int UnknownQuest = 0;
public static final int TourGuide = 1; public static final int TourGuide = 1;
@@ -14,4 +15,6 @@ public class QuestType {
public static final int Tower = 10; public static final int Tower = 10;
public static final int Demon = 11; public static final int Demon = 11;
public static final int TowerEvent = 12; public static final int TowerEvent = 12;
public static final int Weekly = 13;
public static final int Assist = 14;
} }
@@ -11,7 +11,7 @@ public class HandlerDailyShopRewardReceiveReq extends NetHandler {
@Override @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { public byte[] handle(GameSession session, byte[] message) throws Exception {
// Claim daily reward // Claim daily reward
var change = session.getPlayer().getQuestManager().claimDailyReward(); var change = session.getPlayer().getQuestManager().claimDailyShopGift();
if (change == null) { if (change == null) {
return session.encodeMsg(NetMsgId.daily_shop_reward_receive_failed_ack); return session.encodeMsg(NetMsgId.daily_shop_reward_receive_failed_ack);
@@ -13,7 +13,7 @@ public class HandlerQuestDailyActiveRewardReceiveReq extends NetHandler {
@Override @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { public byte[] handle(GameSession session, byte[] message) throws Exception {
// Receive rewards // Receive rewards
var change = session.getPlayer().getQuestManager().claimActiveRewards(); var change = session.getPlayer().getQuestManager().claimDailyActiveRewards();
if (change == null) { if (change == null) {
return session.encodeMsg(NetMsgId.quest_daily_active_reward_receive_failed_ack); return session.encodeMsg(NetMsgId.quest_daily_active_reward_receive_failed_ack);
@@ -15,13 +15,13 @@ public class HandlerQuestDailyRewardReceiveReq extends NetHandler {
var req = UI32.parseFrom(message); var req = UI32.parseFrom(message);
// Receive rewards // Receive rewards
var change = session.getPlayer().getQuestManager().receiveQuestReward(req.getValue()); var change = session.getPlayer().getQuestManager().receiveDailyQuestReward(req.getValue());
if (change == null) { if (change == null) {
return session.encodeMsg(NetMsgId.quest_daily_reward_receive_failed_ack); return session.encodeMsg(NetMsgId.quest_daily_reward_receive_failed_ack);
} }
// Template // Encode and send
return session.encodeMsg(NetMsgId.quest_daily_reward_receive_succeed_ack, change.toProto()); return session.encodeMsg(NetMsgId.quest_daily_reward_receive_succeed_ack, change.toProto());
} }
@@ -0,0 +1,34 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.QuestDailyActiveRewardRecevie.QuestDailyActiveRewardReceiveResp;
import it.unimi.dsi.fastutil.ints.IntList;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.quest_weekly_active_reward_receive_req)
public class HandlerQuestWeeklyActiveRewardReceiveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Receive rewards
var change = session.getPlayer().getQuestManager().claimWeeklyActiveRewards();
if (change == null) {
return session.encodeMsg(NetMsgId.quest_weekly_active_reward_receive_failed_ack);
}
// Build response
var rsp = QuestDailyActiveRewardReceiveResp.newInstance()
.setChange(change.toProto());
if (change.getExtraData() != null && change.getExtraData() instanceof IntList ids) {
ids.forEach(rsp::addActiveIds);
}
// Encode and send
return session.encodeMsg(NetMsgId.quest_weekly_active_reward_receive_succeed_ack, rsp);
}
}
@@ -0,0 +1,28 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.UI32;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.quest_weekly_reward_receive_req)
public class HandlerQuestWeeklyRewardReceiveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = UI32.parseFrom(message);
// Receive rewards
var change = session.getPlayer().getQuestManager().receiveWeeklyQuestReward(req.getValue());
if (change == null) {
return session.encodeMsg(NetMsgId.quest_weekly_reward_receive_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.quest_weekly_reward_receive_succeed_ack, change.toProto());
}
}