Rework how quest/achievement conditions are handled

This commit is contained in:
Melledy
2025-11-30 17:06:08 -08:00
parent a04f3354f7
commit 6f7a92725a
21 changed files with 143 additions and 71 deletions

View File

@@ -18,7 +18,7 @@ For any extra support, questions, or discussions, check out our [Discord](https:
- Shop (only in-game currency supported)
- Commissions
- Heartlink
- Achievements (not all of them are scripted properly)
- Achievements
- Monoliths (completeable but many other features missing)
- Bounty Trials
- Menance Arena

View File

@@ -24,7 +24,7 @@ public class ResourceLoader {
loadResources();
// Add hardcoded achievements params
AchievementHelper.fixParams();
AchievementHelper.init();
// Done
loaded = true;

View File

@@ -20,8 +20,8 @@ public class AchievementDef extends BaseDef {
private int Qty1;
// Custom params
private transient int param1 = -1;
private transient int param2 = -1;
private transient int param1;
private transient int param2;
@Override
public int getId() {
@@ -34,11 +34,11 @@ public class AchievementDef extends BaseDef {
}
public boolean hasParam1() {
return this.param1 >= 0;
return this.param1 > 0;
}
public boolean hasParam2() {
return this.param2 >= 0;
return this.param2 > 0;
}
@Override

View File

@@ -2,29 +2,62 @@ package emu.nebula.game.achievement;
import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AchievementDef;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
// Because achievements in the data files do not have params, we will hardcode them here
public class AchievementHelper {
// Achievement cache
// Cache
private static IntSet isTotalAchievementSet = new IntOpenHashSet();
@Getter
private static Int2ObjectMap<List<AchievementDef>> cache = new Int2ObjectOpenHashMap<>();
public static List<AchievementDef> getAchievementsByCondition(AchievementCondition condition) {
return cache.get(condition.getValue());
public static List<AchievementDef> getAchievementsByCondition(int condition) {
return cache.get(condition);
}
//
public static boolean isTotalAchievement(int condition) {
return isTotalAchievementSet.contains(condition);
}
// Fix params
public static void fixParams() {
addParam(78, -1, 2);
addParam(79, -1, 4);
addParam(498, -1, 1);
public static void init() {
// Cache total achievements
for (var condition : AchievementCondition.values()) {
if (condition.name().endsWith("Total")) {
isTotalAchievementSet.add(condition.getValue());
}
}
isTotalAchievementSet.add(AchievementCondition.ItemsAdd.getValue());
isTotalAchievementSet.add(AchievementCondition.ItemsDeplete.getValue());
// Fix params
fixParams();
}
private static void fixParams() {
// Monolith
addParam(78, 0, 2);
addParam(79, 0, 4);
addParam(498, 0, 1);
// Money
addParam(25, GameConstants.GOLD_ITEM_ID, 0);
addParam(26, GameConstants.GOLD_ITEM_ID, 0);
addParam(27, GameConstants.GOLD_ITEM_ID, 0);
addParam(28, GameConstants.GOLD_ITEM_ID, 0);
addParam(29, GameConstants.GOLD_ITEM_ID, 0);
}
private static void addParam(int achievementId, int param1, int param2) {

View File

@@ -45,6 +45,12 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
this.save();
}
public synchronized int getCompletedAchievementsCount() {
return (int) this.getAchievements().values().stream()
.filter(GameAchievement::isComplete)
.count();
}
/**
* Returns true if there are any unclaimed achievements
*/
@@ -73,6 +79,9 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
}
public synchronized void handleClientEvents(Events events) {
//
boolean hasCompleted = false;
// Parse events
for (var event : events.getList()) {
// Check id
@@ -115,16 +124,31 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
// Set save flag
this.queueSave = true;
// Check if achievement was completed
if (achievement.isComplete()) {
hasCompleted = true;
}
}
}
// Trigger update
if (hasCompleted) {
this.getPlayer().trigger(AchievementCondition.AchievementTotal, this.getCompletedAchievementsCount());
}
}
public synchronized void trigger(AchievementCondition condition, int progress, int param1, int param2) {
public synchronized void trigger(int condition, int progress, int param1, int param2) {
// Sanity check
if (progress <= 0) {
return;
}
// Blacklist
if (condition == AchievementCondition.ClientReport.getValue()) {
return;
}
// Get achievements to trigger
var triggerList = AchievementHelper.getAchievementsByCondition(condition);
@@ -133,7 +157,8 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
}
// Check what type of achievement condition this is
boolean isTotal = condition.name().endsWith("Total");
boolean isTotal = AchievementHelper.isTotalAchievement(condition);
boolean hasCompleted = false;
// Parse achievements
for (var data : triggerList) {
@@ -150,8 +175,18 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
// Set save flag
this.queueSave = true;
// Check if achievement was completed
if (achievement.isComplete()) {
hasCompleted = true;
}
}
}
// Trigger update
if (hasCompleted) {
this.getPlayer().trigger(AchievementCondition.AchievementTotal, this.getCompletedAchievementsCount());
}
}
/**

View File

@@ -75,11 +75,11 @@ public class GameAchievement {
var data = this.getData();
if (data == null) return false;
if (data.hasParam1() && data.getParam1() != param1) {
if ((data.hasParam1() || param1 != 0) && data.getParam1() != param1) {
return false;
}
if (data.hasParam2() && data.getParam2() != param2) {
if ((data.hasParam2() || param2 != 0) && data.getParam2() != param2) {
return false;
}

View File

@@ -85,7 +85,7 @@ public class AgentManager extends PlayerManager implements GameDatabaseObject {
this.getAgents().put(agent.getId(), agent);
// Quest
this.getPlayer().triggerQuest(QuestCondition.AgentApplyTotal, 1);
this.getPlayer().trigger(QuestCondition.AgentApplyTotal, 1);
// Success
return agent;
@@ -184,8 +184,8 @@ public class AgentManager extends PlayerManager implements GameDatabaseObject {
this.save();
// Quest + Achievements
getPlayer().triggerQuest(QuestCondition.AgentFinishTotal, list.size());
getPlayer().triggerAchievement(AchievementCondition.AgentWithSpecificFinishTotal, list.size());
getPlayer().trigger(QuestCondition.AgentFinishTotal, list.size());
getPlayer().trigger(AchievementCondition.AgentWithSpecificFinishTotal, list.size());
// Success
return change.setSuccess(true);

View File

@@ -16,7 +16,6 @@ import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.quest.GameQuest;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.game.quest.QuestType;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.BattlePassInfoOuterClass.BattlePassInfo;
@@ -123,10 +122,10 @@ public class BattlePass implements GameDatabaseObject {
this.save();
}
public synchronized void trigger(QuestCondition condition, int progress, int param) {
public synchronized void trigger(int condition, int progress, int param1, int param2) {
for (var quest : getQuests().values()) {
// Try to trigger quest
boolean result = quest.trigger(condition, progress, param);
boolean result = quest.trigger(condition, progress, param1, param2);
// Skip if quest progress wasn't changed
if (!result) {

View File

@@ -127,7 +127,7 @@ public class CharacterContact {
}
// Trigger quest/achievement
this.getCharacter().getPlayer().triggerAchievement(AchievementCondition.ChatTotal, 1);
this.getCharacter().getPlayer().trigger(AchievementCondition.ChatTotal, 1);
// Success
return change.setSuccess(true);

View File

@@ -18,7 +18,6 @@ import emu.nebula.data.GameData;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.data.resources.TalentGroupDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
@@ -187,7 +186,7 @@ public class GameCharacter implements GameDatabaseObject {
// Check if we leveled up
if (this.level > oldLevel) {
// Trigger quest
this.getPlayer().triggerQuest(QuestCondition.CharacterUpTotal, this.level - oldLevel);
this.getPlayer().trigger(QuestCondition.CharacterUpTotal, this.level - oldLevel);
}
// Save to database
@@ -461,8 +460,7 @@ public class GameCharacter implements GameDatabaseObject {
this.addAffinityExp(exp);
// Trigger quest/achievement
this.getPlayer().triggerQuest(QuestCondition.GiftGiveTotal, count);
this.getPlayer().triggerAchievement(AchievementCondition.GiftGiveTotal, count);
this.getPlayer().trigger(QuestCondition.GiftGiveTotal, count);
// Remove items
var change = this.getPlayer().getInventory().removeItems(items);

View File

@@ -145,7 +145,7 @@ public class GameDisc implements GameDatabaseObject {
// Check if we leveled up
if (this.level > oldLevel) {
// Trigger quest
this.getPlayer().triggerQuest(QuestCondition.DiscStrengthenTotal, this.level - oldLevel);
this.getPlayer().trigger(QuestCondition.DiscStrengthenTotal, this.level - oldLevel);
}
// Save to database

View File

@@ -1,7 +1,6 @@
package emu.nebula.game.dating;
import emu.nebula.data.GameData;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
@@ -28,8 +27,7 @@ public class DatingManager extends PlayerManager {
this.game = new DatingGame(character, data);
// Trigger quest/achievement
this.getPlayer().triggerQuest(QuestCondition.CharactersDatingTotal, 1);
this.getPlayer().triggerAchievement(AchievementCondition.CharactersDatingTotal, 1);
this.getPlayer().trigger(QuestCondition.CharactersDatingTotal, 1);
// Success
return this.game;

View File

@@ -166,7 +166,7 @@ public class GachaModule extends GameContextModule {
player.getGachaManager().addGachaHistory(log);
// Trigger achievements
player.triggerAchievement(AchievementCondition.GachaTotal, amount);
player.trigger(AchievementCondition.GachaTotal, amount);
// Complete
return new GachaResult(info, change, results);

View File

@@ -63,8 +63,8 @@ public class InstanceManager extends PlayerManager {
this.getProgress().saveInstanceLog(log, logName, data.getId(), star);
// Quest triggers
this.getPlayer().triggerQuest(questCondition, 1);
this.getPlayer().triggerQuest(QuestCondition.BattleTotal, 1);
this.getPlayer().trigger(questCondition, 1);
this.getPlayer().trigger(QuestCondition.BattleTotal, 1);
}
// Set extra data
@@ -131,8 +131,8 @@ public class InstanceManager extends PlayerManager {
change.setExtraData(list);
// Quest triggers
this.getPlayer().triggerQuest(questCondition, count);
this.getPlayer().triggerQuest(QuestCondition.BattleTotal, count);
this.getPlayer().trigger(questCondition, count);
this.getPlayer().trigger(QuestCondition.BattleTotal, count);
// Success
return change.setSuccess(true);

View File

@@ -20,6 +20,7 @@ import emu.nebula.proto.Public.Res;
import emu.nebula.proto.Public.Title;
import emu.nebula.proto.Public.UI32;
import emu.nebula.util.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.Int2ObjectMap;
@@ -462,11 +463,11 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
}
}
// Trigger quest
// Trigger quest + achievement
if (amount > 0) {
this.getPlayer().triggerQuest(QuestCondition.ItemsAdd, amount, id);
this.getPlayer().trigger(QuestCondition.ItemsAdd, amount, id);
} else {
this.getPlayer().triggerQuest(QuestCondition.ItemsDeplete, Math.abs(amount), id);
this.getPlayer().trigger(QuestCondition.ItemsDeplete, Math.abs(amount), id);
}
//
@@ -634,6 +635,9 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Add produced items
this.addItem(data.getProductionId(), data.getProductionPerBatch() * num, change);
// Trigger achievement
this.getPlayer().trigger(AchievementCondition.ItemsProductTotal, num);
// Success
return change.setSuccess(true);
}

View File

@@ -216,7 +216,7 @@ public class Player implements GameDatabaseObject {
Nebula.getGameDatabase().update(this, this.getUid(), "level", this.level);
// Trigger achievement
this.triggerAchievement(AchievementCondition.WorldClassSpecific, this.getLevel());
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
}
public void setExp(int exp) {
@@ -512,7 +512,7 @@ public class Player implements GameDatabaseObject {
);
// Trigger achievement
this.triggerAchievement(AchievementCondition.WorldClassSpecific, this.getLevel());
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
}
// Calculate changes
@@ -565,7 +565,7 @@ public class Player implements GameDatabaseObject {
change = modifyEnergy(-amount, change);
// Trigger quest
this.triggerQuest(QuestCondition.EnergyDeplete, amount);
this.trigger(QuestCondition.EnergyDeplete, amount);
// Complete
return change;
@@ -617,8 +617,7 @@ public class Player implements GameDatabaseObject {
this.resetDailies(hasWeekChanged);
// Trigger quest/achievement login
this.triggerQuest(QuestCondition.LoginTotal, 1);
this.triggerAchievement(AchievementCondition.LoginTotal, 1);
this.trigger(QuestCondition.LoginTotal, 1);
// Update last epoch day
this.lastEpochDay = Nebula.getGameContext().getEpochDays();
@@ -633,23 +632,28 @@ public class Player implements GameDatabaseObject {
// Trigger quests + achievements
public void triggerQuest(QuestCondition condition, int progress) {
this.triggerQuest(condition, progress, 0);
}
public void triggerQuest(QuestCondition condition, int progress, int param) {
this.getQuestManager().trigger(condition, progress, param);
this.getBattlePassManager().getBattlePass().trigger(condition, progress, param);
}
public void triggerAchievement(AchievementCondition condition, int progress) {
this.getAchievementManager().trigger(condition, progress, 0, 0);
}
public void triggerAchievement(AchievementCondition condition, int progress, int param1, int param2) {
public void trigger(int condition, int progress, int param1, int param2) {
this.getQuestManager().trigger(condition, progress, param1, param2);
this.getBattlePassManager().getBattlePass().trigger(condition, progress, param1, param2);
this.getAchievementManager().trigger(condition, progress, param1, param2);
}
public void trigger(QuestCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public void trigger(QuestCondition condition, int progress, int param1) {
this.trigger(condition.getValue(), progress, param1, 0);
}
public void trigger(AchievementCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public void trigger(AchievementCondition condition, int progress, int param1, int param2) {
this.trigger(condition.getValue(), progress, param1, param2);
}
// Login
private <T extends PlayerManager> T loadManagerFromDatabase(Class<T> cls) {

View File

@@ -55,19 +55,19 @@ public class GameQuest {
return 0;
}
public boolean trigger(QuestCondition condition, int progress, int param) {
public boolean trigger(int condition, int progress, int param1, int param2) {
// Sanity check
if (this.isComplete()) {
return false;
}
// Skip if not the correct condition
if (this.cond != condition.getValue()) {
if (this.cond != condition) {
return false;
}
// Check quest param
if (this.param != 0 && param != this.param) {
if (this.param != 0 && param1 != this.param) {
return false;
}

View File

@@ -114,10 +114,10 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
this.save();
}
public synchronized void trigger(QuestCondition condition, int progress, int param) {
public synchronized void trigger(int condition, int progress, int param1, int param2) {
for (var quest : getQuests().values()) {
// Try to trigger quest
boolean result = quest.trigger(condition, progress, param);
boolean result = quest.trigger(condition, progress, param1, param2);
// Skip if quest progress wasn't changed
if (!result) {
@@ -194,7 +194,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
}
// Trigger quest
this.getPlayer().triggerQuest(QuestCondition.QuestWithSpecificType, claimList.size(), QuestType.Daily);
this.getPlayer().trigger(QuestCondition.QuestWithSpecificType, claimList.size(), QuestType.Daily);
// Success
return change.setSuccess(true);
@@ -305,7 +305,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
Nebula.getGameDatabase().update(this, this.getUid(), "hasDailyReward", this.hasDailyReward);
// Trigger quest
this.getPlayer().triggerQuest(QuestCondition.DailyShopReceiveShopTotal, 1);
this.getPlayer().trigger(QuestCondition.DailyShopReceiveShopTotal, 1);
// Success
return change.setSuccess(true);

View File

@@ -208,7 +208,7 @@ public class StarTowerManager extends PlayerManager {
this.game = new StarTowerGame(this, data, formation, req);
// Trigger quest
this.getPlayer().triggerQuest(QuestCondition.TowerEnterFloor, 1);
this.getPlayer().trigger(QuestCondition.TowerEnterFloor, 1);
// Success
return change.setExtraData(this.game);
@@ -228,13 +228,14 @@ public class StarTowerManager extends PlayerManager {
// Handle victory events
if (victory) {
// Trigger achievements
this.getPlayer().triggerAchievement(
this.getPlayer().trigger(AchievementCondition.TowerClearTotal, 1);
this.getPlayer().trigger(
AchievementCondition.TowerClearSpecificGroupIdAndDifficulty,
1,
game.getData().getGroupId(),
game.getData().getDifficulty()
);
this.getPlayer().triggerAchievement(
this.getPlayer().trigger(
AchievementCondition.TowerClearSpecificLevelWithDifficultyAndTotal,
1,
game.getData().getId(),

View File

@@ -176,7 +176,7 @@ public class VampireSurvivorManager extends PlayerManager {
this.game = null;
// Trigger achievement
getPlayer().triggerAchievement(AchievementCondition.VampireWithSpecificClearTotal, 1);
getPlayer().trigger(AchievementCondition.VampireWithSpecificClearTotal, 1);
}
private void updateSavedCards() {

View File

@@ -13,7 +13,7 @@ public class HandlerClientEventReportReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Interact
session.getPlayer().triggerQuest(QuestCondition.ClientReport, 1, 1005);
session.getPlayer().trigger(QuestCondition.ClientReport, 1, 1005);
// Encode response
return session.encodeMsg(NetMsgId.client_event_report_succeed_ack, Nil.newInstance());