mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-15 08:25:21 +01:00
Simply implement achievement system and add achievement command (#2068)
* Implement achievement system * Update src/main/java/emu/grasscutter/command/commands/AchievementCommand.java Co-authored-by: Der Chien <b03902015@ntu.edu.tw> * fix: redundant codes * fix: redundant codes * Update language files --------- Co-authored-by: Der Chien <b03902015@ntu.edu.tw>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package emu.grasscutter.game.achievement;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import emu.grasscutter.net.proto.AchievementOuterClass;
|
||||
import emu.grasscutter.net.proto.StatusOuterClass;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
public class Achievement {
|
||||
@Setter
|
||||
private StatusOuterClass.Status status;
|
||||
private int id;
|
||||
private int totalProgress;
|
||||
@Setter
|
||||
private int curProgress;
|
||||
@Setter
|
||||
private int finishTimestampSec;
|
||||
|
||||
public Achievement(StatusOuterClass.Status status, int id, int totalProgress, int curProgress, int finishTimestampSec) {
|
||||
this.status = status;
|
||||
this.id = id;
|
||||
this.totalProgress = totalProgress;
|
||||
this.curProgress = curProgress;
|
||||
this.finishTimestampSec = finishTimestampSec;
|
||||
}
|
||||
|
||||
public AchievementOuterClass.Achievement toProto() {
|
||||
return AchievementOuterClass.Achievement.newBuilder()
|
||||
.setStatus(this.getStatus())
|
||||
.setId(this.getId())
|
||||
.setTotalProgress(this.getTotalProgress())
|
||||
.setCurProgress(this.getCurProgress())
|
||||
.setFinishTimestamp(this.getFinishTimestampSec())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package emu.grasscutter.game.achievement;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class AchievementControlReturns {
|
||||
private final Return ret;
|
||||
private final int changedAchievementStatusNum;
|
||||
|
||||
private AchievementControlReturns(Return ret) {
|
||||
this(ret, 0);
|
||||
}
|
||||
|
||||
private AchievementControlReturns(Return ret, int changedAchievementStatusNum) {
|
||||
this.ret = ret;
|
||||
this.changedAchievementStatusNum = changedAchievementStatusNum;
|
||||
}
|
||||
|
||||
public static AchievementControlReturns success(int changedAchievementStatusNum) {
|
||||
return new AchievementControlReturns(Return.SUCCESS, changedAchievementStatusNum);
|
||||
}
|
||||
|
||||
public static AchievementControlReturns achievementNotFound() {
|
||||
return new AchievementControlReturns(Return.ACHIEVEMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static AchievementControlReturns alreadyAchieved() {
|
||||
return new AchievementControlReturns(Return.ALREADY_ACHIEVED);
|
||||
}
|
||||
|
||||
public static AchievementControlReturns notYetAchieved() {
|
||||
return new AchievementControlReturns(Return.NOT_YET_ACHIEVED);
|
||||
}
|
||||
|
||||
public enum Return {
|
||||
SUCCESS("commands.achievement.success."),
|
||||
ACHIEVEMENT_NOT_FOUND("commands.achievement.fail.achievement_not_found"),
|
||||
ALREADY_ACHIEVED("commands.achievement.fail.already_achieved"),
|
||||
NOT_YET_ACHIEVED("commands.achievement.fail.not_yet_achieved");
|
||||
|
||||
@Getter
|
||||
private final String key;
|
||||
|
||||
Return(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
290
src/main/java/emu/grasscutter/game/achievement/Achievements.java
Normal file
290
src/main/java/emu/grasscutter/game/achievement/Achievements.java
Normal file
@@ -0,0 +1,290 @@
|
||||
package emu.grasscutter.game.achievement;
|
||||
|
||||
import com.github.davidmoten.guavamini.Lists;
|
||||
import dev.morphia.annotations.Entity;
|
||||
import dev.morphia.annotations.Id;
|
||||
import dev.morphia.annotations.Transient;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.AchievementData;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.net.proto.StatusOuterClass;
|
||||
import emu.grasscutter.server.packet.send.PacketAchievementAllDataNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketAchievementUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketTakeAchievementGoalRewardRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketTakeAchievementRewardRsp;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
@Entity("achievements")
|
||||
@Data
|
||||
@Builder(builderMethodName = "of")
|
||||
public class Achievements {
|
||||
private static final IntSupplier currentTimeSecs = () -> (int) (System.currentTimeMillis() / 1000L);
|
||||
private static final Achievement INVALID = new Achievement(StatusOuterClass.Status.STATUS_INVALID, -1, 0, 0, 0);
|
||||
@Id
|
||||
private ObjectId id;
|
||||
private int uid;
|
||||
@Transient
|
||||
private Player player;
|
||||
private Map<Integer, Achievement> achievementList;
|
||||
@Getter
|
||||
private int finishedAchievementNum;
|
||||
private List<Integer> takenGoalRewardIdList;
|
||||
|
||||
public static Achievements getByPlayer(Player player) {
|
||||
var achievements = player.getAchievements() == null ? DatabaseHelper.getAchievementData(player.getUid()) : player.getAchievements();
|
||||
if (achievements == null) {
|
||||
achievements = create(player.getUid());
|
||||
}
|
||||
return achievements;
|
||||
}
|
||||
|
||||
public static Achievements create(int uid) {
|
||||
var newAchievement = Achievements.of()
|
||||
.uid(uid)
|
||||
.achievementList(init())
|
||||
.finishedAchievementNum(0)
|
||||
.takenGoalRewardIdList(Lists.newArrayList())
|
||||
.build();
|
||||
newAchievement.save();
|
||||
return newAchievement;
|
||||
}
|
||||
|
||||
private static Map<Integer, Achievement> init() {
|
||||
Map<Integer, Achievement> map = new HashMap<>();
|
||||
GameData.getAchievementDataMap().values().stream()
|
||||
.filter(AchievementData::isUsed)
|
||||
.forEach(a -> {
|
||||
map.put(a.getId(), new Achievement(StatusOuterClass.Status.STATUS_UNFINISHED, a.getId(), a.getProgress(), 0, 0));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
public AchievementControlReturns grant(int achievementId) {
|
||||
var a = this.getAchievement(achievementId);
|
||||
|
||||
if (a == null || this.isFinished(achievementId)) {
|
||||
return a == null ? AchievementControlReturns.achievementNotFound() : AchievementControlReturns.alreadyAchieved();
|
||||
}
|
||||
|
||||
return this.progress(achievementId, a.getTotalProgress());
|
||||
}
|
||||
|
||||
public AchievementControlReturns revoke(int achievementId) {
|
||||
var a = this.getAchievement(achievementId);
|
||||
|
||||
if (a == null || !this.isFinished(achievementId)) {
|
||||
return a == null ? AchievementControlReturns.achievementNotFound() : AchievementControlReturns.notYetAchieved();
|
||||
}
|
||||
|
||||
return this.progress(achievementId, 0);
|
||||
}
|
||||
|
||||
public AchievementControlReturns progress(int achievementId, int progress) {
|
||||
var a = this.getAchievement(achievementId);
|
||||
if (a == null) {
|
||||
return AchievementControlReturns.achievementNotFound();
|
||||
}
|
||||
|
||||
a.setCurProgress(progress);
|
||||
return AchievementControlReturns.success(this.notifyOtherAchievements(a));
|
||||
}
|
||||
|
||||
private int notifyOtherAchievements(Achievement a) {
|
||||
var changedNum = new AtomicInteger();
|
||||
|
||||
changedNum.addAndGet(this.update(a) ? 1 : 0);
|
||||
|
||||
GameData.getAchievementDataMap().get(a.getId())
|
||||
.getExcludedGroupAchievementIdList().stream()
|
||||
.map(this::getAchievement)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(other -> {
|
||||
other.setCurProgress(a.getCurProgress());
|
||||
changedNum.addAndGet(this.update(other) ? 1 : 0);
|
||||
});
|
||||
|
||||
this.computeFinishedAchievementNum();
|
||||
this.save();
|
||||
this.sendUpdatePacket(a);
|
||||
return changedNum.intValue();
|
||||
}
|
||||
|
||||
private boolean update(Achievement a) {
|
||||
if (a.getStatus() == StatusOuterClass.Status.STATUS_UNFINISHED && a.getCurProgress() >= a.getTotalProgress()) {
|
||||
a.setStatus(StatusOuterClass.Status.STATUS_FINISHED);
|
||||
a.setFinishTimestampSec(currentTimeSecs.getAsInt());
|
||||
return true;
|
||||
} else if (this.isFinished(a.getId()) && a.getCurProgress() < a.getTotalProgress()) {
|
||||
a.setStatus(StatusOuterClass.Status.STATUS_UNFINISHED);
|
||||
a.setFinishTimestampSec(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void computeFinishedAchievementNum() {
|
||||
this.finishedAchievementNum = GameData.getAchievementDataMap().values().stream()
|
||||
.filter(a -> this.isFinished(a.getId()))
|
||||
.mapToInt(value -> 1)
|
||||
.sum();
|
||||
}
|
||||
|
||||
private void sendUpdatePacket(Achievement achievement) {
|
||||
List<Achievement> achievements = Lists.newArrayList(achievement);
|
||||
achievements.addAll(GameData.getAchievementDataMap().get(achievement.getId())
|
||||
.getExcludedGroupAchievementIdList()
|
||||
.stream().map(this::getAchievement)
|
||||
.filter(Objects::nonNull)
|
||||
.toList()
|
||||
);
|
||||
|
||||
this.sendUpdatePacket(achievements);
|
||||
}
|
||||
|
||||
private void sendUpdatePacket(List<Achievement> achievement) {
|
||||
if (this.isPacketSendable()) {
|
||||
this.player.sendPacket(new PacketAchievementUpdateNotify(achievement));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Achievement getAchievement(int achievementId) {
|
||||
if (this.isInvalid(achievementId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getAchievementList().computeIfAbsent(achievementId, id -> {
|
||||
return new Achievement(StatusOuterClass.Status.STATUS_UNFINISHED, id, GameData.getAchievementDataMap().get(id.intValue()).getProgress(), 0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isInvalid(int achievementId) {
|
||||
var data = GameData.getAchievementDataMap().get(achievementId);
|
||||
return data == null || data.isDisuse();
|
||||
}
|
||||
|
||||
public StatusOuterClass.Status getStatus(int achievementId) {
|
||||
return this.getAchievementList().getOrDefault(achievementId, INVALID).getStatus();
|
||||
}
|
||||
|
||||
public boolean isFinished(int achievementId) {
|
||||
var status = this.getStatus(achievementId);
|
||||
return status == StatusOuterClass.Status.STATUS_FINISHED || status == StatusOuterClass.Status.STATUS_REWARD_TAKEN;
|
||||
}
|
||||
|
||||
public void takeReward(List<Integer> ids) {
|
||||
List<GameItem> rewards = Lists.newArrayList();
|
||||
|
||||
for (int i : ids) {
|
||||
var target = GameData.getAchievementDataMap().get(i);
|
||||
if (target == null) {
|
||||
Grasscutter.getLogger().warn("null returned while taking reward!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isRewardTaken(i)) {
|
||||
this.player.sendPacket(new PacketTakeAchievementRewardRsp());
|
||||
return;
|
||||
}
|
||||
|
||||
var data = GameData.getRewardDataMap().get(target.getFinishRewardId());
|
||||
if (data == null) {
|
||||
Grasscutter.getLogger().warn("null returned while getting reward data!");
|
||||
continue;
|
||||
}
|
||||
|
||||
data.getRewardItemList().forEach(itemParamData -> {
|
||||
var itemData = GameData.getItemDataMap().get(itemParamData.getId());
|
||||
if (itemData == null) {
|
||||
Grasscutter.getLogger().warn("itemData == null!");
|
||||
return;
|
||||
}
|
||||
|
||||
rewards.add(new GameItem(itemData, itemParamData.getCount()));
|
||||
});
|
||||
|
||||
var a = this.getAchievement(i);
|
||||
a.setStatus(StatusOuterClass.Status.STATUS_REWARD_TAKEN);
|
||||
this.save();
|
||||
this.sendUpdatePacket(a);
|
||||
}
|
||||
|
||||
this.player.getInventory().addItems(rewards, ActionReason.AchievementReward);
|
||||
this.player.sendPacket(new PacketTakeAchievementRewardRsp(ids, rewards.stream().map(GameItem::toItemParam).toList()));
|
||||
}
|
||||
|
||||
public void takeGoalReward(List<Integer> ids) {
|
||||
List<GameItem> rewards = Lists.newArrayList();
|
||||
|
||||
for (int i : ids) {
|
||||
if (this.takenGoalRewardIdList.contains(i)) {
|
||||
this.player.sendPacket(new PacketTakeAchievementGoalRewardRsp());
|
||||
}
|
||||
|
||||
var goalData = GameData.getAchievementGoalDataMap().get(i);
|
||||
if (goalData == null) {
|
||||
Grasscutter.getLogger().warn("null returned while getting goal reward data!");
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = GameData.getRewardDataMap().get(goalData.getFinishRewardId());
|
||||
if (data == null) {
|
||||
Grasscutter.getLogger().warn("null returned while getting reward data!");
|
||||
continue;
|
||||
}
|
||||
|
||||
data.getRewardItemList().forEach(itemParamData -> {
|
||||
var itemData = GameData.getItemDataMap().get(itemParamData.getId());
|
||||
if (itemData == null) {
|
||||
Grasscutter.getLogger().warn("itemData == null!");
|
||||
return;
|
||||
}
|
||||
|
||||
rewards.add(new GameItem(itemData, itemParamData.getCount()));
|
||||
});
|
||||
|
||||
this.takenGoalRewardIdList.add(i);
|
||||
this.save();
|
||||
}
|
||||
|
||||
this.player.getInventory().addItems(rewards, ActionReason.AchievementGoalReward);
|
||||
this.player.sendPacket(new PacketTakeAchievementGoalRewardRsp(ids, rewards.stream().map(GameItem::toItemParam).toList()));
|
||||
}
|
||||
|
||||
public boolean isRewardTaken(int achievementId) {
|
||||
return this.getStatus(achievementId) == StatusOuterClass.Status.STATUS_REWARD_TAKEN;
|
||||
}
|
||||
|
||||
public boolean isRewardLeft(int achievementId) {
|
||||
return this.getStatus(achievementId) == StatusOuterClass.Status.STATUS_FINISHED;
|
||||
}
|
||||
|
||||
private boolean isPacketSendable() {
|
||||
return this.player != null;
|
||||
}
|
||||
|
||||
public void save() {
|
||||
DatabaseHelper.saveAchievementData(this);
|
||||
}
|
||||
|
||||
public void onLogin(Player player) {
|
||||
if (this.player == null) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
this.player.sendPacket(new PacketAchievementAllDataNotify(this.player));
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.game.CoopRequest;
|
||||
import emu.grasscutter.game.ability.AbilityManager;
|
||||
import emu.grasscutter.game.achievement.Achievements;
|
||||
import emu.grasscutter.game.activity.ActivityManager;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.avatar.AvatarStorage;
|
||||
@@ -168,6 +169,7 @@ public class Player {
|
||||
@Getter private transient SatiationManager satiationManager;
|
||||
|
||||
// Manager data (Save-able to the database)
|
||||
@Getter private transient Achievements achievements;
|
||||
private PlayerProfile playerProfile; // Getter has null-check
|
||||
@Getter private TeamManager teamManager;
|
||||
private TowerData towerData; // Getter has null-check
|
||||
@@ -973,11 +975,15 @@ public class Player {
|
||||
.setIsShowAvatar(this.isShowAvatars())
|
||||
.addAllShowAvatarInfoList(socialShowAvatarInfoList)
|
||||
.addAllShowNameCardIdList(this.getShowNameCardInfoList())
|
||||
.setFinishAchievementNum(0)
|
||||
.setFinishAchievementNum(this.getFinishedAchievementNum())
|
||||
.setFriendEnterHomeOptionValue(this.getHome() == null ? 0 : this.getHome().getEnterHomeOption());
|
||||
return social;
|
||||
}
|
||||
|
||||
public int getFinishedAchievementNum() {
|
||||
return Achievements.getByPlayer(this).getFinishedAchievementNum();
|
||||
}
|
||||
|
||||
public List<ShowAvatarInfoOuterClass.ShowAvatarInfo> getShowAvatarInfoList() {
|
||||
List<ShowAvatarInfoOuterClass.ShowAvatarInfo> showAvatarInfoList = new ArrayList<>();
|
||||
|
||||
@@ -1172,6 +1178,7 @@ public class Player {
|
||||
}
|
||||
|
||||
// Load from db
|
||||
this.achievements = Achievements.getByPlayer(this);
|
||||
this.getAvatars().loadFromDatabase();
|
||||
this.getInventory().loadFromDatabase();
|
||||
this.loadBattlePassManager(); // Call before avatar postLoad to avoid null pointer
|
||||
@@ -1224,6 +1231,10 @@ public class Player {
|
||||
session.send(new PacketQuestListNotify(this));
|
||||
session.send(new PacketCodexDataFullNotify(this));
|
||||
session.send(new PacketAllWidgetDataNotify(this));
|
||||
|
||||
//Achievements
|
||||
this.achievements.onLogin(this);
|
||||
|
||||
session.send(new PacketWidgetGadgetAllDataNotify());
|
||||
session.send(new PacketCombineDataNotify(this.unlockedCombines));
|
||||
session.send(new PacketGetChatEmojiCollectionRsp(this.getChatEmojiIdList()));
|
||||
|
||||
Reference in New Issue
Block a user