Improve achievement params handling

This commit is contained in:
Melledy
2025-12-16 00:11:18 -08:00
parent 85b08c7593
commit d9b502002c
24 changed files with 1768 additions and 229 deletions

View File

@@ -60,10 +60,7 @@ public class BuildCommand implements CommandHandler {
var build = builder.toBuild();
// Add to star tower manager
target.getStarTowerManager().getBuilds().put(build.getUid(), build);
// Save to database
build.save();
target.getStarTowerManager().addBuild(build);
// Send package to player
target.addNextPackage(NetMsgId.st_import_build_notify, build.toProto());

View File

@@ -83,6 +83,13 @@ public class CharacterCommand implements CommandHandler {
}
player.addNextPackage(NetMsgId.chars_final_notify, proto);
// Trigger achievements
if (args.getLevel() > 0 && args.getLevel() <= 90) {
player.getCharacters().triggerCharacterAchievements(modified, args.getLevel());
}
// Result
return "Updated " + modified.size() + " character(s)";
}
}

View File

@@ -122,6 +122,7 @@ public class GameData {
@Getter private static DataTable<StarTowerFloorExpDef> StarTowerFloorExpDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerTeamExpDef> StarTowerTeamExpDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerEventDef> StarTowerEventDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerBuildRankDef> StarTowerBuildRankDataTable = new DataTable<>();
@Getter private static DataTable<SubNoteSkillPromoteGroupDef> SubNoteSkillPromoteGroupDataTable = new DataTable<>();
@Getter private static DataTable<PotentialDef> PotentialDataTable = new DataTable<>();

View File

@@ -0,0 +1,33 @@
package emu.nebula.data.custom;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = "AchievementParam.json", useInternal = true, loadPriority = LoadPriority.LOW)
public class AchievementParamDef extends BaseDef {
private int Id;
private int Param1;
private int ParamCond1;
private int Param2;
private int ParamCond2;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
var achievement = GameData.getAchievementDataTable().get(this.Id);
if (achievement == null) {
return;
}
achievement.setParams(this);
}
}

View File

@@ -2,7 +2,9 @@ package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.data.custom.AchievementParamDef;
import emu.nebula.game.achievement.AchievementHelper;
import emu.nebula.game.achievement.AchievementParamCond;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.Getter;
@@ -20,43 +22,50 @@ public class AchievementDef extends BaseDef {
private int Qty1;
// Custom params
private transient int param1; // -1 == any, 0 = no param, 1+ = param required
private transient int param2; // -1 == any, 0 = no param, 1+ = param required
private transient int param1;
private transient AchievementParamCond paramCond1;
private transient int param2;
private transient AchievementParamCond paramCond2;
@Override
public int getId() {
return Id;
}
@Deprecated
public void setParams(int param1, int param2) {
this.param1 = param1;
this.param2 = param2;
}
public void setParams(AchievementParamDef params) {
this.param1 = params.getParam1();
this.param2 = params.getParam2();
this.paramCond1 = AchievementParamCond.getByValue(params.getParamCond1());
this.paramCond2 = AchievementParamCond.getByValue(params.getParamCond2());
}
/**
* Checks if this achievement requires params to match
*/
public boolean hasParam1(int param) {
if (this.param1 < 0) {
return false;
} else if (this.param1 == 0) {
return param != 0;
} else {
return true;
public boolean matchParam1(int value) {
if (this.paramCond1 == null) {
this.paramCond1 = AchievementParamCond.EQUALS;
}
return this.paramCond1.test(this.param1, value);
}
/**
* Checks if this achievement requires params to match
*/
public boolean hasParam2(int param) {
if (this.param2 < 0) {
return false;
} else if (this.param2 == 0) {
return param != 0;
} else {
return true;
public boolean matchParam2(int value) {
if (this.paramCond2 == null) {
this.paramCond2 = AchievementParamCond.EQUALS;
}
return this.paramCond2.test(this.param2, value);
}
@Override

View File

@@ -4,9 +4,9 @@ import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.custom.CharGemAttrGroupDef;
import emu.nebula.util.CustomIntArray;
import emu.nebula.util.Utils;
import emu.nebula.util.WeightedList;
import emu.nebula.util.ints.CustomIntArray;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;

View File

@@ -4,7 +4,6 @@ import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.util.WeightedList;
import lombok.Getter;
@Getter

View File

@@ -0,0 +1,18 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "StarTowerBuildRank.json")
public class StarTowerBuildRankDef extends BaseDef {
private int Id;
private int MinGrade;
private int Rarity;
@Override
public int getId() {
return Id;
}
}

View File

@@ -7,7 +7,7 @@ import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import emu.nebula.util.String2IntMap;
import emu.nebula.util.ints.String2IntMap;
/**
* Custom mongodb codec for encoding/decoding fastutil int2int maps.

View File

@@ -2,14 +2,13 @@ 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 emu.nebula.game.tower.room.RoomType;
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
@@ -47,120 +46,5 @@ public class AchievementHelper {
incrementalAchievementSet.add(AchievementCondition.TowerItemsGet.getValue());
incrementalAchievementSet.add(AchievementCondition.TowerEnterRoom.getValue());
// Fix params
fixParams();
}
private static void fixParams() {
// Clear "Misstep On One"
addParam(56, 401, 0); // Custom trigger
// Clear "Currents and Shadows"
addParam(57, 102, 0);
addParam(58, 103, 0);
addParam(59, 104, 0);
addParam(60, 105, 0);
addParam(61, 106, 0);
addParam(62, 107, 0);
addParam(63, 108, 0);
// Clear "Dust and Flames"
addParam(64, 202, 0);
addParam(65, 203, 0);
addParam(66, 204, 0);
addParam(67, 205, 0);
addParam(68, 206, 0);
addParam(69, 207, 0);
addParam(70, 208, 0);
// Clear "Storm and Thunder"
addParam(71, 302, 0);
addParam(72, 303, 0);
addParam(73, 304, 0);
addParam(74, 305, 0);
addParam(75, 306, 0);
addParam(76, 307, 0);
addParam(77, 308, 0);
// First Ascension
addParam(498, 0, 1);
// Monolith Conqueror
addParam(78, 2, 0);
addParam(79, 4, 0);
addParam(80, 6, 0);
addParam(81, 7, 0);
// 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);
// Ininfite tower
for (int diff = 10, id = 270; diff <= 60; diff += 10) {
addParam(id++, 11000 + diff, 0); // Infinite Arena
addParam(id++, 51000 + diff, 0); // Shake the Floor
addParam(id++, 41000 + diff, 0); // Elegance and Flow
addParam(id++, 71000 + diff, 0); // Upbeat Party
addParam(id++, 31000 + diff, 0); // Thrilling Beat
addParam(id++, 21000 + diff, 0); // Flames and Beats
addParam(id++, 61000 + diff, 0); // Sinister Ritual
}
// Character count
addParams(393, 398, 1, 0);
// Disc count
addParams(382, 387, 1, 0);
// Star Tower team clear
addParams(95, 98, 1, 0); // Aqua team clear
addParams(99, 102, 2, 0); // Fire team clear
addParams(103, 106, 3, 0); // Earth team clear
addParams(107, 110, 4, 0); // Wind team clear
addParams(111, 114, 5, 0); // Light team clear
addParams(115, 118, 6, 0); // Dark team clear
// Star tower items
addParams(139, 144, GameConstants.TOWER_COIN_ITEM_ID, 0);
addParams(145, 149, 90011, 0);
addParams(150, 154, 90012, 0);
addParams(155, 159, 90013, 0);
addParams(160, 164, 90014, 0);
addParams(165, 169, 90015, 0);
addParams(170, 174, 90016, 0);
addParams(175, 179, 90017, 0);
addParams(180, 184, 90018, 0);
addParams(185, 189, 90019, 0);
addParams(190, 194, 90020, 0);
addParams(195, 199, 90021, 0);
addParams(200, 204, 90022, 0);
addParams(205, 209, 90023, 0);
// Star tower rooms
addParams(210, 216, RoomType.BattleRoom.getValue() + 1, 0);
addParams(217, 223, RoomType.EliteBattleRoom.getValue() + 1, 0);
addParams(224, 230, RoomType.BossRoom.getValue() + 1, 0);
addParams(231, 237, RoomType.FinalBossRoom.getValue() + 1, 0);
addParams(238, 244, RoomType.ShopRoom.getValue() + 1, 0);
addParams(245, 251, RoomType.EventRoom.getValue() + 1, 0);
}
private static void addParam(int achievementId, int param1, int param2) {
var data = GameData.getAchievementDataTable().get(achievementId);
if (data == null) return;
data.setParams(param1, param2);
}
private static void addParams(int start, int end, int param1, int param2) {
for (int id = start; id <= end; id++) {
addParam(id, param1, param2);
}
}
}

View File

@@ -0,0 +1,46 @@
package emu.nebula.game.achievement;
import emu.nebula.util.ints.IntBiPredicate;
import lombok.Getter;
@Getter
public enum AchievementParamCond {
EQUALS (0, (param, value) -> value == param),
ANY (1, (param, value) -> true),
NOT_EQUALS (2, (param, value) -> value != param),
GREATER_THAN (3, (param, value) -> value > param),
GREATER_THAN_OR_EQ (4, (param, value) -> value >= param),
LESS_THAN (5, (param, value) -> value < param),
LESS_THAN_OR_EQ (6, (param, value) -> value <= param);
private final int value;
private final IntBiPredicate operation;
// Static cache
private static AchievementParamCond[] CACHE;
static {
CACHE = new AchievementParamCond[AchievementParamCond.values().length];
for (AchievementParamCond type : AchievementParamCond.values()) {
CACHE[type.getValue()] = type;
}
}
private AchievementParamCond(int value, IntBiPredicate operation) {
this.value = value;
this.operation = operation;
}
public boolean test(int param, int value) {
return this.getOperation().test(param, value);
}
public static AchievementParamCond getByValue(int value) {
try {
return CACHE[value];
} catch (Exception e) {
return AchievementParamCond.EQUALS;
}
}
}

View File

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

View File

@@ -12,8 +12,10 @@ import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.HandbookInfo;
import emu.nebula.util.Bitset;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@@ -117,6 +119,67 @@ public class CharacterStorage extends PlayerManager {
return handbook;
}
public void triggerCharacterAchievements(GameCharacter character) {
// Create elements we want to trigger the achievement for
var elements = new ItemParamMap();
elements.put(character.getData().getElementType().getValue(), 0);
// Calculate the amount of characters with the elements
this.triggerCharacterAchievements(elements, character.getLevel());
}
public void triggerCharacterAchievements(Collection<GameCharacter> characters, int level) {
var elements = new ItemParamMap();
for (var character : characters) {
elements.put(character.getElementType().getValue(), 0);
}
// Calculate the amount of characters with the elements
this.triggerCharacterAchievements(elements, level);
}
private void triggerCharacterAchievements(ItemParamMap elements, int level) {
// Get current amount of characters with same level or higher
int anyCount = 0;
for (var character : this.getCharacterCollection()) {
if (character.getLevel() >= level) {
anyCount++;
} else {
continue;
}
int element = character.getElementType().getValue();
if (elements.containsKey(element)) {
elements.add(element, 1);
}
}
// Trigger achievements
var achievements = this.getPlayer().getAchievementManager();
achievements.trigger(
AchievementCondition.CharactersWithSpecificLevelAndQuantity,
anyCount,
level,
0
);
for (var entry : elements) {
int element = entry.getIntKey();
int amount = entry.getIntValue();
achievements.trigger(
AchievementCondition.CharactersWithSpecificNumberLevelAndAttributes,
amount,
level,
element
);
}
}
// Discs
public GameDisc getDiscById(int id) {

View File

@@ -35,7 +35,7 @@ import emu.nebula.proto.PublicStarTower.StarTowerChar;
import emu.nebula.proto.PublicStarTower.StarTowerCharGem;
import emu.nebula.server.error.ServerException;
import emu.nebula.util.Bitset;
import emu.nebula.util.CustomIntArray;
import emu.nebula.util.ints.CustomIntArray;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
@@ -119,6 +119,10 @@ public class GameCharacter implements GameDatabaseObject {
}
}
public ElementType getElementType() {
return this.getData().getElementType();
}
public boolean isMaster() {
return this.getData().getGrade() == 1;
}
@@ -193,6 +197,8 @@ public class GameCharacter implements GameDatabaseObject {
if (this.level > oldLevel) {
// Trigger quest
this.getPlayer().trigger(QuestCondition.CharacterUpTotal, this.level - oldLevel);
// Trigger any achievements
this.getPlayer().getCharacters().triggerCharacterAchievements(this);
}
// Save to database

View File

@@ -76,6 +76,9 @@ public class GachaModule extends GameContextModule {
var transformItemsDst = new ItemParamMap();
var bonusItems = new ItemParamMap();
// Character count (for achievements)
int characters = 0;
// Add for player
for (var entry : acquireItems.getItems().int2ObjectEntrySet()) {
// Get ids and aquire params
@@ -102,6 +105,9 @@ public class GachaModule extends GameContextModule {
transformItemsDst.add(characterData.getFragmentsId(), characterData.getTransformQty() * count);
transformItemsDst.add(24, 40 * count); // Expert permits
}
// Add to count
characters += acquire.getCount();
} else if (acquire.getType() == ItemType.Disc) {
// Get add amount
int begin = acquire.getBegin();
@@ -167,6 +173,7 @@ public class GachaModule extends GameContextModule {
// Trigger achievements
player.trigger(AchievementCondition.GachaTotal, amount);
player.trigger(AchievementCondition.GachaCharacterTotal, characters);
// Complete
return new GachaResult(info, change, results);

View File

@@ -20,7 +20,7 @@ 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.util.String2IntMap;
import emu.nebula.util.ints.String2IntMap;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;

View File

@@ -9,6 +9,7 @@ import dev.morphia.annotations.PostLoad;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
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;
@@ -103,6 +104,9 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject {
// Add rewards
this.getPlayer().getInventory().addItems(data.getRewards(), change);
// Trigger any achievement/quests
this.getPlayer().trigger(AchievementCondition.StoryClear, 1, id, 0);
// Save to db
Nebula.getGameDatabase().addToSet(this, this.getPlayerUid(), "completedStories", id);
}

View File

@@ -8,6 +8,7 @@ import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.SecondarySkillDef;
import emu.nebula.data.resources.StarTowerBuildRankDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
@@ -162,6 +163,19 @@ public class StarTowerBuild implements GameDatabaseObject {
return this.score;
}
public StarTowerBuildRankDef getRank() {
StarTowerBuildRankDef rank = null;
// TODO optimize
for (var data : GameData.getStarTowerBuildRankDataTable()) {
if (this.getScore() >= data.getMinGrade()) {
rank = data;
}
}
return rank;
}
// Proto
public StarTowerBuildInfo toProto() {

View File

@@ -166,6 +166,100 @@ public class StarTowerManager extends PlayerManager {
return this.getBuilds().containsKey(id);
}
public void addBuild(StarTowerBuild build) {
// Add to builds
this.getBuilds().put(build.getUid(), build);
// Save build to database
build.save();
// Achievement
var rank = build.getRank();
if (rank != null) {
this.getPlayer().getAchievementManager().trigger(
AchievementCondition.TowerBuildSpecificScoreWithTotal,
1,
rank.getRarity(),
0
);
}
}
public PlayerChangeInfo saveBuild(boolean delete, String name, boolean lock) {
// Sanity check
if (this.getLastBuild() == null) {
return null;
}
// Create player change info
var change = new PlayerChangeInfo();
// Cache build
var build = this.lastBuild;
// Clear reference to build
this.lastBuild = null;
// Check if the player wants this build or not
if (delete) {
return this.dismantleBuild(build, change);
}
// Check limit
if (this.getBuilds().size() >= 50) {
return null;
}
// Add build
this.addBuild(build);
// Success
return change;
}
private PlayerChangeInfo dismantleBuild(StarTowerBuild build, PlayerChangeInfo change) {
// Calculate quanity of tickets from record score
int count = (int) Math.floor(build.getScore() / 100);
// Check weekly tickets
int maxAmount = this.getPlayer().getProgress().getMaxEarnableWeeklyTowerTickets();
count = Math.min(maxAmount, count);
// Add journey tickets
this.getPlayer().getInventory().addItem(12, count, change);
// Add to weekly ticket log
this.getPlayer().getProgress().addWeeklyTowerTicketLog(count);
// Success
return change;
}
public PlayerChangeInfo deleteBuild(long buildId, PlayerChangeInfo change) {
// Create change info
if (change == null) {
change = new PlayerChangeInfo();
}
// Get build
var build = this.getBuilds().remove(buildId);
if (build == null) {
return change;
}
// Delete
build.delete();
// Add journey tickets
this.dismantleBuild(build, change);
// Success
return change;
}
// Game
public PlayerChangeInfo apply(StarTowerApplyReq req) {
// Sanity checks
var data = GameData.getStarTowerDataTable().get(req.getId());
@@ -297,84 +391,6 @@ public class StarTowerManager extends PlayerManager {
);
}
// Build
private PlayerChangeInfo dismantleBuild(StarTowerBuild build, PlayerChangeInfo change) {
// Calculate quanity of tickets from record score
int count = (int) Math.floor(build.getScore() / 100);
// Check weekly tickets
int maxAmount = this.getPlayer().getProgress().getMaxEarnableWeeklyTowerTickets();
count = Math.min(maxAmount, count);
// Add journey tickets
this.getPlayer().getInventory().addItem(12, count, change);
// Add to weekly ticket log
this.getPlayer().getProgress().addWeeklyTowerTicketLog(count);
// Success
return change;
}
public PlayerChangeInfo saveBuild(boolean delete, String name, boolean lock) {
// Sanity check
if (this.getLastBuild() == null) {
return null;
}
// Create player change info
var change = new PlayerChangeInfo();
// Cache build
var build = this.lastBuild;
// Clear reference to build
this.lastBuild = null;
// Check if the player wants this build or not
if (delete) {
return this.dismantleBuild(build, change);
}
// Check limit
if (this.getBuilds().size() >= 50) {
return null;
}
// Add to builds
this.getBuilds().put(build.getUid(), build);
// Save build to database
build.save();
// Success
return change;
}
public PlayerChangeInfo deleteBuild(long buildId, PlayerChangeInfo change) {
// Create change info
if (change == null) {
change = new PlayerChangeInfo();
}
// Get build
var build = this.getBuilds().remove(buildId);
if (build == null) {
return change;
}
// Delete
build.delete();
// Add journey tickets
this.dismantleBuild(build, change);
// Success
return change;
}
// Database
public void loadFromDatabase() {

View File

@@ -9,7 +9,6 @@ import emu.nebula.data.resources.VampireSurvivorDef;
import emu.nebula.proto.Public.CardInfo;
import emu.nebula.proto.Public.VampireSurvivorLevelReward;
import emu.nebula.util.WeightedList;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;

View File

@@ -1,4 +1,4 @@
package emu.nebula.util;
package emu.nebula.util.ints;
import it.unimi.dsi.fastutil.ints.IntArrayList;

View File

@@ -0,0 +1,15 @@
package emu.nebula.util.ints;
import java.util.function.BiPredicate;
public interface IntBiPredicate extends BiPredicate<Integer, Integer>{
@Deprecated
@Override
default boolean test(Integer t, Integer u) {
return test(t, u);
}
public boolean test(int t, int u);
}

View File

@@ -1,4 +1,4 @@
package emu.nebula.util;
package emu.nebula.util.ints;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;

File diff suppressed because it is too large Load Diff