diff --git a/common/src/main/java/earth/terrarium/heracles/api/quests/Quest.java b/common/src/main/java/earth/terrarium/heracles/api/quests/Quest.java index ff33956b..82d0f945 100644 --- a/common/src/main/java/earth/terrarium/heracles/api/quests/Quest.java +++ b/common/src/main/java/earth/terrarium/heracles/api/quests/Quest.java @@ -51,11 +51,13 @@ public void claimAllowedRewards(ServerPlayer player, String id) { QuestsProgress progresses = QuestProgressHandler.getProgress(player.server, player.getUUID()); QuestProgress progress = progresses.getProgress(id); if (progress == null) return; + if (!progress.isUnlocked()) return; if (!progress.isComplete() && !this.tasks.isEmpty()) return; claimRewards(player, id, progresses, progress); } public void claimRewards(ServerPlayer player, String id, QuestsProgress progresses, QuestProgress progress) { + if (!progress.isUnlocked()) return; if (progress.isClaimed(this)) return; claimRewards( @@ -71,6 +73,7 @@ public void claimRewards(ServerPlayer player, String id, QuestsProgress progress public void claimAllowedReward(ServerPlayer player, String id, String rewardId) { QuestsProgress progress = QuestProgressHandler.getProgress(player.server, player.getUUID()); if (!progress.isComplete(id) && !this.tasks.isEmpty()) return; + if (!progress.isUnlocked(id)) return; if (progress.isClaimed(id, this)) return; var questProgress = progress.getProgress(id); diff --git a/common/src/main/java/earth/terrarium/heracles/api/quests/QuestProgressionMode.java b/common/src/main/java/earth/terrarium/heracles/api/quests/QuestProgressionMode.java new file mode 100644 index 00000000..5ee340cf --- /dev/null +++ b/common/src/main/java/earth/terrarium/heracles/api/quests/QuestProgressionMode.java @@ -0,0 +1,32 @@ +package earth.terrarium.heracles.api.quests; + +import net.minecraft.util.StringRepresentable; +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; + +/** + * Specifies when a player can make progress on a quest. + *

+ * This is inspired by a similar setting in FTB Quests. + */ +public enum QuestProgressionMode implements StringRepresentable { + /** + * Players can make progress on this quest’s tasks only if the quest is unlocked. + */ + LINEAR, + /** + * Players can make progress on this quest’s tasks at any time + * but can claim the rewards only once it is unlocked. + */ + FLEXIBLE; + + @Override + public @NotNull String getSerializedName() { + return "quest.heracles.%s".formatted(name().toLowerCase(Locale.ROOT)); + } + + public boolean isFlexible() { + return this == FLEXIBLE; + } +} diff --git a/common/src/main/java/earth/terrarium/heracles/api/quests/QuestSettings.java b/common/src/main/java/earth/terrarium/heracles/api/quests/QuestSettings.java index f6f455e5..b6b5719e 100644 --- a/common/src/main/java/earth/terrarium/heracles/api/quests/QuestSettings.java +++ b/common/src/main/java/earth/terrarium/heracles/api/quests/QuestSettings.java @@ -13,7 +13,8 @@ public final class QuestSettings { Codec.BOOL.fieldOf("unlockNotification").orElse(false).forGetter(QuestSettings::unlockNotification), Codec.BOOL.fieldOf("showDependencyArrow").orElse(true).forGetter(QuestSettings::showDependencyArrow), Codec.BOOL.fieldOf("repeatable").orElse(false).forGetter(QuestSettings::repeatable), - Codec.BOOL.fieldOf("autoClaimRewards").orElse(false).forGetter(QuestSettings::autoClaimRewards) + Codec.BOOL.fieldOf("autoClaimRewards").orElse(false).forGetter(QuestSettings::autoClaimRewards), + EnumCodec.of(QuestProgressionMode.class).fieldOf("progression_mode").orElse(QuestProgressionMode.LINEAR).forGetter(QuestSettings::progressionMode) ).apply(instance, QuestSettings::new)); private boolean individualProgress; @@ -22,18 +23,20 @@ public final class QuestSettings { private boolean showDependencyArrow; private boolean repeatable; private boolean autoClaimRewards; + private QuestProgressionMode progressionMode; - public QuestSettings(boolean individualProgress, QuestDisplayStatus hiddenUntil, boolean unlockNotification, boolean showDependencyArrow, boolean repeatable, boolean autoClaimRewards) { + public QuestSettings(boolean individualProgress, QuestDisplayStatus hiddenUntil, boolean unlockNotification, boolean showDependencyArrow, boolean repeatable, boolean autoClaimRewards, QuestProgressionMode progressionMode) { this.individualProgress = individualProgress; this.hiddenUntil = hiddenUntil; this.unlockNotification = unlockNotification; this.showDependencyArrow = showDependencyArrow; this.repeatable = repeatable; this.autoClaimRewards = autoClaimRewards; + this.progressionMode = progressionMode; } public static QuestSettings createDefault() { - return new QuestSettings(false, QuestDisplayStatus.LOCKED, false, true, false, false); + return new QuestSettings(false, QuestDisplayStatus.LOCKED, false, true, false, false, QuestProgressionMode.LINEAR); } public boolean individualProgress() { @@ -60,6 +63,10 @@ public boolean autoClaimRewards() { return autoClaimRewards; } + public QuestProgressionMode progressionMode() { + return progressionMode; + } + @Override public boolean equals(Object obj) { if (obj == this) return true; @@ -71,12 +78,13 @@ public boolean equals(Object obj) { this.unlockNotification == that.unlockNotification && this.showDependencyArrow == that.showDependencyArrow && this.repeatable == that.repeatable && - this.autoClaimRewards == that.autoClaimRewards; + this.autoClaimRewards == that.autoClaimRewards && + this.progressionMode == that.progressionMode; } @Override public int hashCode() { - return Objects.hash(individualProgress, hiddenUntil, unlockNotification, showDependencyArrow, repeatable, autoClaimRewards); + return Objects.hash(individualProgress, hiddenUntil, unlockNotification, showDependencyArrow, repeatable, autoClaimRewards, progressionMode); } public void update(QuestSettings newSettings) { @@ -85,6 +93,7 @@ public void update(QuestSettings newSettings) { this.unlockNotification = newSettings.unlockNotification; this.showDependencyArrow = newSettings.showDependencyArrow; this.repeatable = newSettings.repeatable; + this.progressionMode = newSettings.progressionMode; } public void setIndividualProgress(boolean individualProgress) { @@ -111,4 +120,8 @@ public void setAutoClaimRewards(boolean autoClaimRewards) { this.autoClaimRewards = autoClaimRewards; } + public void setProgressionMode(QuestProgressionMode progressionMode) { + this.progressionMode = progressionMode; + } + } diff --git a/common/src/main/java/earth/terrarium/heracles/api/tasks/QuestTask.java b/common/src/main/java/earth/terrarium/heracles/api/tasks/QuestTask.java index beedd782..6ae6659f 100644 --- a/common/src/main/java/earth/terrarium/heracles/api/tasks/QuestTask.java +++ b/common/src/main/java/earth/terrarium/heracles/api/tasks/QuestTask.java @@ -4,10 +4,22 @@ import net.minecraft.nbt.Tag; import net.minecraft.server.level.ServerPlayer; +/** + * A task of a quest. + *

+ * This contains the data about the particular task in addition to the generic type. + * + * @see QuestTaskType + * @param the type of the input to test against + * @param the type of the NBT tag used to record progress + * @param the type implementing this interface + */ public interface QuestTask> { /** * The id of the task. + *

+ * This is only unique within a single quest. * * @return The id. */ diff --git a/common/src/main/java/earth/terrarium/heracles/client/HeraclesClient.java b/common/src/main/java/earth/terrarium/heracles/client/HeraclesClient.java index 210a52ee..38ecc461 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/HeraclesClient.java +++ b/common/src/main/java/earth/terrarium/heracles/client/HeraclesClient.java @@ -68,8 +68,8 @@ public static void displayItemsRewardedToast(String id, List items) { QuestClaimedToast.addOrUpdate(Minecraft.getInstance().getToasts(), id, items); } - public static void displayQuestCompleteToast(String id) { - QuestCompletedToast.add(Minecraft.getInstance().getToasts(), id); + public static void displayQuestCompleteToast(String id, boolean provisional) { + QuestCompletedToast.add(Minecraft.getInstance().getToasts(), id, provisional); } public static void displayQuestUnlockedToast(String id) { diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/quest/BaseQuestScreen.java b/common/src/main/java/earth/terrarium/heracles/client/screens/quest/BaseQuestScreen.java index 266498c1..c771d811 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/screens/quest/BaseQuestScreen.java +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/quest/BaseQuestScreen.java @@ -193,7 +193,12 @@ protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int @Override public @NotNull Component getTitle() { - return content.progress().isComplete() ? Component.translatable("gui.heracles.quest.title.complete", super.getTitle()) : super.getTitle(); + if (content.progress().isComplete()) { + return Component.translatable( + content.progress().isUnlocked() ? "gui.heracles.quest.title.complete" : "gui.heracles.quest.title.provisional", + super.getTitle()); + } + return super.getTitle(); } public Quest quest() { diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/quest/rewards/RewardListWidget.java b/common/src/main/java/earth/terrarium/heracles/client/screens/quest/rewards/RewardListWidget.java index 9c701be0..dc00db38 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/screens/quest/rewards/RewardListWidget.java +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/quest/rewards/RewardListWidget.java @@ -95,7 +95,9 @@ public void update(String group, String id, Quest quest) { DisplayWidget widget = QuestRewardWidgets.create(reward); if (widget == null) continue; - if (progress.canClaim(reward.id())) { + if (!progress.isUnlocked()) { + locked.add(new MutablePair<>(reward, widget)); + } else if (progress.canClaim(reward.id())) { available.add(new MutablePair<>(reward, widget)); } else if (progress.isComplete()) { claimed.add(new MutablePair<>(reward, widget)); diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestSettingsInitalizer.java b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestSettingsInitalizer.java index b9f99911..81f729a1 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestSettingsInitalizer.java +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestSettingsInitalizer.java @@ -4,6 +4,7 @@ import earth.terrarium.heracles.api.client.settings.base.BooleanSetting; import earth.terrarium.heracles.api.client.settings.base.EnumSetting; import earth.terrarium.heracles.api.quests.QuestDisplayStatus; +import earth.terrarium.heracles.api.quests.QuestProgressionMode; import earth.terrarium.heracles.api.quests.QuestSettings; import org.jetbrains.annotations.Nullable; @@ -20,6 +21,7 @@ public CreationData create(@Nullable QuestSettings object) { settings.put("show_dependency_arrow", BooleanSetting.TRUE, object != null && object.showDependencyArrow()); settings.put("repeatable", BooleanSetting.FALSE, object != null && object.repeatable()); settings.put("auto_claim_rewards", BooleanSetting.FALSE, object != null && object.autoClaimRewards()); + settings.put("progression_mode", new EnumSetting<>(QuestProgressionMode.class, QuestProgressionMode.LINEAR), object != null ? object.progressionMode() : QuestProgressionMode.LINEAR); return settings; } @@ -31,7 +33,8 @@ public QuestSettings create(String id, QuestSettings object, Data data) { data.get("unlock_notification", BooleanSetting.FALSE).orElse(object != null && object.unlockNotification()), data.get("show_dependency_arrow", BooleanSetting.TRUE).orElse(object != null && object.showDependencyArrow()), data.get("repeatable", BooleanSetting.FALSE).orElse(object != null && object.repeatable()), - data.get("auto_claim_rewards", BooleanSetting.FALSE).orElse(object != null && object.autoClaimRewards()) + data.get("auto_claim_rewards", BooleanSetting.FALSE).orElse(object != null && object.autoClaimRewards()), + data.get("progression_mode", new EnumSetting<>(QuestProgressionMode.class, QuestProgressionMode.LINEAR)).orElse(object != null ? object.progressionMode() : QuestProgressionMode.LINEAR) ); } } diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestWidget.java b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestWidget.java index aadcd5d8..93dc7154 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestWidget.java +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestWidget.java @@ -20,6 +20,7 @@ public class QuestWidget { + private static final int NUM_STATUSES = ModUtils.QuestStatus.values().length; private final ClientQuests.QuestEntry entry; private final Quest quest; private final ModUtils.QuestStatus status; @@ -45,15 +46,15 @@ public void render(GuiGraphics graphics, ScissorBoxStack scissor, int x, int y, x + x() + info.xOffset(), y + y() + info.yOffset(), status.ordinal() * info.width(), 0, info.width(), info.height(), - info.width() * 5, info.height() + info.width() * (NUM_STATUSES + 1), info.height() ); if (hovered) { graphics.blit(quest.display().iconBackground(), x + x() + info.xOffset(), y + y() + info.yOffset(), - 4 * info.width(), 0, + NUM_STATUSES * info.width(), 0, info.width(), info.height(), - info.width() * 5, info.height() + info.width() * (NUM_STATUSES + 1), info.height() ); } RenderSystem.disableBlend(); diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/SelectQuestWidget.java b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/SelectQuestWidget.java index c8557ffc..41e79c3c 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/SelectQuestWidget.java +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/SelectQuestWidget.java @@ -164,7 +164,8 @@ public SelectQuestWidget(int x, int y, int width, int height, QuestsWidget widge .unlockNotification(questSettings.unlockNotification()) .showDependencyArrow(questSettings.showDependencyArrow()) .repeatable(questSettings.repeatable()) - .autoClaimRewards(questSettings.autoClaimRewards()); + .autoClaimRewards(questSettings.autoClaimRewards()) + .progressionMode(questSettings.progressionMode()); }) ); edit.setTitle(Component.translatable("gui.heracles.quests.edit_quest_settings")); diff --git a/common/src/main/java/earth/terrarium/heracles/client/toasts/QuestCompletedToast.java b/common/src/main/java/earth/terrarium/heracles/client/toasts/QuestCompletedToast.java index aafb4e45..c07658cf 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/toasts/QuestCompletedToast.java +++ b/common/src/main/java/earth/terrarium/heracles/client/toasts/QuestCompletedToast.java @@ -17,10 +17,16 @@ public class QuestCompletedToast extends WrappingHintToast implements Toast { private static final Component TITLE_TEXT = Component.translatable("quest.heracles.toast"); private static final Component KEY_HINT = Component.translatable("quest.heracles.toast.desc", Component.keybind("key.heracles.open_quests") .withStyle(style -> style.withBold(true).withColor(ToastsTheme.getKeybinding()))); + private static final Component TITLE_TEXT_PROVISIONAL = Component.translatable("quest.heracles.toast.provisional"); + private static final Component KEY_HINT_PROVISIONAL = Component.translatable("quest.heracles.toast.provisional.desc"); private final QuestIcon icon; - public QuestCompletedToast(Quest quest) { - super(TITLE_TEXT, List.of(quest.display().title()), List.of(KEY_HINT),5000L); + public QuestCompletedToast(Quest quest, boolean provisional) { + super( + provisional ? TITLE_TEXT_PROVISIONAL : TITLE_TEXT, + List.of(quest.display().title()), + List.of(provisional ? KEY_HINT_PROVISIONAL : KEY_HINT), + 5000L); this.icon = quest.display().icon(); } @@ -32,9 +38,9 @@ public Toast.Visibility render(GuiGraphics graphics, ToastComponent toastCompone return visible; } - public static void add(ToastComponent toastComponent, String quest) { + public static void add(ToastComponent toastComponent, String quest, boolean provisional) { ClientQuests.get(quest).ifPresent(entry -> - toastComponent.addToast(new QuestCompletedToast(entry.value())) + toastComponent.addToast(new QuestCompletedToast(entry.value(), provisional)) ); } } diff --git a/common/src/main/java/earth/terrarium/heracles/common/handlers/pinned/PinnedQuestHandler.java b/common/src/main/java/earth/terrarium/heracles/common/handlers/pinned/PinnedQuestHandler.java index 6fb9063e..cae52d37 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/handlers/pinned/PinnedQuestHandler.java +++ b/common/src/main/java/earth/terrarium/heracles/common/handlers/pinned/PinnedQuestHandler.java @@ -24,7 +24,7 @@ public static Set getPinned(ServerPlayer player) { return read(player.server).pinned.computeIfAbsent(player.getUUID(), u -> new LinkedHashSet<>()); } - public static void syncIfChanged(ServerPlayer player, Collection pinned) { + public static void syncIfChanged(ServerPlayer player, Iterable pinned) { Set pinnedSet = getPinned(player); for (String s : pinned) { if (!pinnedSet.contains(s)) { diff --git a/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestProgress.java b/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestProgress.java index 7eab6ee7..e7ae3887 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestProgress.java +++ b/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestProgress.java @@ -9,19 +9,25 @@ import java.util.*; +/** + * Records a player’s progress for an individual quest. + */ public class QuestProgress { private final Map> tasks = new HashMap<>(); private final Set claimed = new HashSet<>(); private boolean complete; + private boolean unlocked; public QuestProgress() { this.complete = false; + this.unlocked = false; } public QuestProgress(Quest quest, CompoundTag tag) { if (tag == null) return; this.complete = tag.getBoolean("complete"); + this.unlocked = tag.getBoolean("unlocked"); this.claimed.addAll(TagUtils.mapToCollection(ArrayList::new, tag.getList("rewards", 8), Tag::getAsString)); var compound = tag.getCompound("tasks"); for (String taskKey : compound.getAllKeys()) { @@ -64,6 +70,14 @@ public void setComplete(boolean complete) { this.complete = complete; } + public boolean isUnlocked() { + return unlocked; + } + + public void setUnlocked(boolean unlocked) { + this.unlocked = unlocked; + } + public void claimReward(String reward) { claimed.add(reward); } @@ -85,7 +99,7 @@ public boolean isClaimed(Quest quest) { } public boolean canClaim(String reward) { - return !claimed.contains(reward) && complete; + return !claimed.contains(reward) && complete && unlocked; } @SuppressWarnings("unchecked") @@ -103,12 +117,14 @@ public void copyFrom(QuestProgress progress) { tasks.clear(); tasks.putAll(progress.tasks); complete = progress.complete; + unlocked = progress.unlocked; checkComplete(); } public CompoundTag save() { CompoundTag tag = new CompoundTag(); tag.putBoolean("complete", complete); + tag.putBoolean("unlocked", unlocked); tag.put("rewards", TagUtils.mapToListTag(claimed, StringTag::valueOf)); CompoundTag tasks = new CompoundTag(); for (var entry : this.tasks.entrySet()) { diff --git a/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestProgressHandler.java b/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestProgressHandler.java index 05aa8e45..22a89796 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestProgressHandler.java +++ b/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestProgressHandler.java @@ -52,7 +52,7 @@ public static void setupChanger() { }); } - public static void sync(ServerPlayer player, Collection quests) { + public static void sync(ServerPlayer player, Iterable quests) { Map progress = new LinkedHashMap<>(); quests.forEach(id -> { Quest quest = QuestHandler.get(id); diff --git a/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestsProgress.java b/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestsProgress.java index f5d84a2e..911bb9a2 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestsProgress.java +++ b/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/QuestsProgress.java @@ -21,6 +21,11 @@ import java.util.*; +/** + * Records a player’s progress for all quests. + * @param progress a map between quest IDs and the progress for the corresponding quest + * @param completableQuests a record of which quests a player can make progress toward + */ public record QuestsProgress(Map progress, CompletableQuests completableQuests) { public QuestsProgress(Map progress) { @@ -29,7 +34,8 @@ public QuestsProgress(Map progress) { public > void testAndProgressTaskType(ServerPlayer player, I input, QuestTaskType taskType) { List editedQuests = new ArrayList<>(); - for (String id : this.completableQuests.getQuests(this)) { + for (var e : this.completableQuests.getProgressableQuestEntries(this)) { + String id = e.id(); QuestProgress questProgress = getProgress(id); Quest quest = QuestHandler.get(id); QuestEntry entry = QuestEntry.of(id, quest); @@ -51,7 +57,7 @@ public QuestsProgress(Map progress) { questProgress.update(quest); this.progress.put(id, questProgress); if (questProgress.isComplete()) { - sendOutQuestComplete(entry, player); + sendOutQuestComplete(entry, player, e.provisional()); } } if (editedQuests.isEmpty()) return; @@ -95,7 +101,7 @@ public void claimReward(String questId, String rewardId, ServerPlayer player) { } public > boolean testAndProgressTask(ServerPlayer player, String id, String task, I input, QuestTaskType taskType) { - List completableQuests = this.completableQuests.getQuests(this); + Collection completableQuests = this.completableQuests.getQuests(this); if (!completableQuests.contains(id)) return false; QuestProgress questProgress = getProgress(id); Quest quest = QuestHandler.get(id); @@ -116,7 +122,7 @@ public void sendOutQuestChanged(String id, Quest quest, QuestProgress questProgr PinnedQuestHandler.syncIfChanged(player, List.of(id)); QuestEntry entry = QuestEntry.of(id, quest); if (questProgress.isComplete()) { - sendOutQuestComplete(entry, player); + sendOutQuestComplete(entry, player, !questProgress.isUnlocked()); } this.completableQuests.updateCompleteQuests(this, player); syncToTeam(player, List.of(entry)); @@ -135,19 +141,19 @@ private void syncToTeam(ServerPlayer player, List quests) { var newTasks = copyTasks(questProgress.tasks()); memberProgress.progress.put(entry.id(), new QuestProgress(questProgress.isComplete(), Set.copyOf(Optionull.mapOrDefault(currentProgress, QuestProgress::claimedRewards, new HashSet<>())), newTasks)); if (serverPlayer != null && (questProgress.isComplete() && !wasComplete)) { - sendOutQuestComplete(entry, player); + sendOutQuestComplete(entry, player, !questProgress.isUnlocked()); } } memberProgress.completableQuests.updateCompleteQuests(memberProgress, serverPlayer); - List questIds = memberProgress.completableQuests.getQuests(memberProgress); + Iterable questIds = memberProgress.completableQuests.getProgressableQuests(memberProgress); if (serverPlayer != null) { QuestProgressHandler.sync(serverPlayer, questIds); } }); } - public static void sendOutQuestComplete(QuestEntry entry, ServerPlayer player) { - NetworkHandler.CHANNEL.sendToPlayer(new QuestCompletedPacket(entry.id()), player); + public static void sendOutQuestComplete(QuestEntry entry, ServerPlayer player, boolean provisional) { + NetworkHandler.CHANNEL.sendToPlayer(new QuestCompletedPacket(entry.id(), provisional), player); HeraclesEvents.QuestCompleteListener.fire(QuestEventTarget.create(entry, player)); } @@ -163,6 +169,34 @@ public boolean isComplete(String id) { return Optionull.mapOrDefault(progress.get(id), QuestProgress::isComplete, false); } + public boolean isUnlocked(String id) { + return Optionull.mapOrDefault(progress.get(id), QuestProgress::isUnlocked, false); + } + + public void setUnlocked(String id, boolean unlocked) { + this.progress.compute( + id, + (k, v) -> { + if (v == null) { + if (unlocked) { + QuestProgress progress = new QuestProgress(); + progress.setUnlocked(true); + return progress; + } else { + return null; + } + } else { + v.setUnlocked(true); + return v; + } + } + ); + } + + public boolean calculateUnlockedStatus(Quest quest) { + return quest.dependencies().stream().allMatch(this::isComplete); + } + public boolean isClaimed(String id, Quest quest) { QuestProgress progress = this.progress.get(id); if (progress == null) return true; diff --git a/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/TaskProgress.java b/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/TaskProgress.java index 582236e5..99c059b3 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/TaskProgress.java +++ b/common/src/main/java/earth/terrarium/heracles/common/handlers/progress/TaskProgress.java @@ -6,6 +6,10 @@ import java.util.function.Supplier; +/** + * Records a player’s progress toward a task within a quest. + * @param the type of the NBT tag used to record progress + */ public class TaskProgress { private S progress; diff --git a/common/src/main/java/earth/terrarium/heracles/common/handlers/quests/CompletableQuests.java b/common/src/main/java/earth/terrarium/heracles/common/handlers/quests/CompletableQuests.java index 2aae3d84..863ff6e2 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/handlers/quests/CompletableQuests.java +++ b/common/src/main/java/earth/terrarium/heracles/common/handlers/quests/CompletableQuests.java @@ -1,5 +1,6 @@ package earth.terrarium.heracles.common.handlers.quests; +import com.google.common.collect.Iterables; import earth.terrarium.heracles.api.quests.Quest; import earth.terrarium.heracles.api.tasks.QuestTask; import earth.terrarium.heracles.common.handlers.progress.QuestProgress; @@ -12,49 +13,68 @@ import net.minecraft.server.level.ServerPlayer; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.BiConsumer; public class CompletableQuests { private boolean updated = false; - private final List quests = new ArrayList<>(); + private final Set quests = new HashSet<>(); + private final Set provisionalQuests = new HashSet<>(); - public List getQuests(QuestsProgress progress) { + public Collection getQuests(QuestsProgress progress) { if (!this.updated) { this.updateCompleteQuests(progress); } return this.quests; } + public Iterable getProgressableQuests(QuestsProgress progress) { + if (!this.updated) { + this.updateCompleteQuests(progress); + } + return Iterables.concat(this.quests, this.provisionalQuests); + } + + public Iterable getProgressableQuestEntries(QuestsProgress progress) { + if (!this.updated) { + this.updateCompleteQuests(progress); + } + return Iterables.concat( + Iterables.transform(this.quests, id -> new Entry(id, false)), + Iterables.transform(this.provisionalQuests, id -> new Entry(id, true))); + } + public void updateCompleteQuests(QuestsProgress progress, BiConsumer onUnlocked) { this.updated = true; + this.provisionalQuests.clear(); List tempQuests = new ArrayList<>(); for (var entry : QuestHandler.quests().entrySet()) { Quest quest = entry.getValue(); String id = entry.getKey(); - if (progress.isComplete(id)) continue; + boolean complete = progress.isComplete(id); + boolean flexible = quest.settings().progressionMode().isFlexible(); + boolean previouslyUnlocked = progress.isUnlocked(id); if (quest.tasks().isEmpty()) continue; if (quest.dependencies().isEmpty()) { - tempQuests.add(id); - if (!this.quests.contains(id)) { + if (!this.quests.contains(id) && !previouslyUnlocked) { onUnlocked.accept(id, quest); } + progress.setUnlocked(id, true); + if (complete) continue; + tempQuests.add(id); } else { - boolean complete = true; - for (String dependency : quest.dependencies()) { - if (!progress.isComplete(dependency)) { - complete = false; - break; + boolean unlocked = progress.calculateUnlockedStatus(quest); + progress.setUnlocked(id, unlocked); + if (unlocked) { + if (!this.quests.contains(id) && !previouslyUnlocked) { + onUnlocked.accept(id, quest); } - } - if (complete) { + if (complete) continue; tempQuests.add(id); - if (!this.quests.contains(id)) { - onUnlocked.accept(id, quest); + } else if (flexible) { + if (!complete) { + provisionalQuests.add(id); } } } @@ -114,4 +134,5 @@ private static T initTask(QuestTask task, QuestProgress } private record UpdatedEntry(String id, Quest quest, Map newProgress) {} + public record Entry(String id, boolean provisional) {} } diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/QuestCompletedPacket.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/QuestCompletedPacket.java index 737e086e..4fddd0eb 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/network/packets/QuestCompletedPacket.java +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/QuestCompletedPacket.java @@ -8,7 +8,7 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; -public record QuestCompletedPacket(String id) implements Packet { +public record QuestCompletedPacket(String id, boolean provisional) implements Packet { public static final ClientboundPacketType TYPE = new Type(); @Override @@ -30,16 +30,17 @@ public ResourceLocation id() { @Override public void encode(QuestCompletedPacket message, FriendlyByteBuf buffer) { buffer.writeUtf(message.id); + buffer.writeBoolean(message.provisional); } @Override public QuestCompletedPacket decode(FriendlyByteBuf buffer) { - return new QuestCompletedPacket(buffer.readUtf()); + return new QuestCompletedPacket(buffer.readUtf(), buffer.readBoolean()); } @Override public Runnable handle(QuestCompletedPacket message) { - return () -> HeraclesClient.displayQuestCompleteToast(message.id()); + return () -> HeraclesClient.displayQuestCompleteToast(message.id(), message.provisional()); } } } diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/data/NetworkQuestData.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/data/NetworkQuestData.java index d55f1b66..19314ff8 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/data/NetworkQuestData.java +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/data/NetworkQuestData.java @@ -3,10 +3,7 @@ import com.teamresourceful.bytecodecs.base.ByteCodec; import com.teamresourceful.bytecodecs.base.object.ObjectByteCodec; import com.teamresourceful.resourcefullib.common.utils.TriState; -import earth.terrarium.heracles.api.quests.GroupDisplay; -import earth.terrarium.heracles.api.quests.Quest; -import earth.terrarium.heracles.api.quests.QuestDisplayStatus; -import earth.terrarium.heracles.api.quests.QuestIcon; +import earth.terrarium.heracles.api.quests.*; import earth.terrarium.heracles.api.rewards.QuestReward; import earth.terrarium.heracles.api.rewards.QuestRewards; import earth.terrarium.heracles.api.tasks.QuestTask; @@ -70,6 +67,7 @@ public static class Builder { private TriState showDependencyArrow = TriState.UNDEFINED; private TriState repeatable = TriState.UNDEFINED; private TriState autoClaimRewards = TriState.UNDEFINED; + private QuestProgressionMode progressionMode = null; private Set dependencies; private Map> tasks; private Map> rewards; @@ -145,6 +143,11 @@ public Builder autoClaimRewards(boolean autoClaimRewards) { return this; } + public Builder progressionMode(QuestProgressionMode progressionMode) { + this.progressionMode = progressionMode; + return this; + } + public Builder dependencies(Set dependencies) { this.dependencies = new HashSet<>(dependencies); return this; @@ -173,14 +176,15 @@ public NetworkQuestData build() { ); } NetworkQuestSettingsData settings = null; - if (individualProgress != TriState.UNDEFINED || hiddenUntil != null || unlockNotification != TriState.UNDEFINED) { + if (individualProgress != TriState.UNDEFINED || hiddenUntil != null || unlockNotification != TriState.UNDEFINED || showDependencyArrow != TriState.UNDEFINED || repeatable != TriState.UNDEFINED || autoClaimRewards != TriState.UNDEFINED || progressionMode != null) { settings = new NetworkQuestSettingsData( Optional.ofNullable(individualProgress.isUndefined() ? null : individualProgress.isTrue()), Optional.ofNullable(hiddenUntil), Optional.ofNullable(unlockNotification.isUndefined() ? null : unlockNotification.isTrue()), Optional.ofNullable(showDependencyArrow.isUndefined() ? null : showDependencyArrow.isTrue()), Optional.ofNullable(repeatable.isUndefined() ? null : repeatable.isTrue()), - Optional.ofNullable(autoClaimRewards.isUndefined() ? null : autoClaimRewards.isTrue()) + Optional.ofNullable(autoClaimRewards.isUndefined() ? null : autoClaimRewards.isTrue()), + Optional.ofNullable(progressionMode) ); } diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/data/NetworkQuestSettingsData.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/data/NetworkQuestSettingsData.java index eccc83ca..cc11b2f6 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/data/NetworkQuestSettingsData.java +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/data/NetworkQuestSettingsData.java @@ -4,6 +4,7 @@ import com.teamresourceful.bytecodecs.base.object.ObjectByteCodec; import earth.terrarium.heracles.api.quests.Quest; import earth.terrarium.heracles.api.quests.QuestDisplayStatus; +import earth.terrarium.heracles.api.quests.QuestProgressionMode; import earth.terrarium.heracles.api.quests.QuestSettings; import java.util.Optional; @@ -14,7 +15,8 @@ public record NetworkQuestSettingsData( Optional unlockNotification, Optional showDependencyArrow, Optional repeatable, - Optional autoClaimRewards + Optional autoClaimRewards, + Optional progressionMode ) { public static final ByteCodec CODEC = ObjectByteCodec.create( @@ -24,6 +26,7 @@ public record NetworkQuestSettingsData( ByteCodec.BOOLEAN.optionalFieldOf(NetworkQuestSettingsData::showDependencyArrow), ByteCodec.BOOLEAN.optionalFieldOf(NetworkQuestSettingsData::repeatable), ByteCodec.BOOLEAN.optionalFieldOf(NetworkQuestSettingsData::autoClaimRewards), + ByteCodec.ofEnum(QuestProgressionMode.class).optionalFieldOf(NetworkQuestSettingsData::progressionMode), NetworkQuestSettingsData::new ); @@ -35,5 +38,6 @@ public void update(Quest quest) { showDependencyArrow.ifPresent(settings::setShowDependencyArrow); repeatable.ifPresent(settings::setRepeatable); autoClaimRewards.ifPresent(settings::setAutoClaimRewards); + progressionMode.ifPresent(settings::setProgressionMode); } } diff --git a/common/src/main/java/earth/terrarium/heracles/common/utils/ModUtils.java b/common/src/main/java/earth/terrarium/heracles/common/utils/ModUtils.java index c57d0907..f8404b01 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/utils/ModUtils.java +++ b/common/src/main/java/earth/terrarium/heracles/common/utils/ModUtils.java @@ -11,6 +11,7 @@ import earth.terrarium.heracles.Heracles; import earth.terrarium.heracles.common.handlers.progress.QuestProgressHandler; import earth.terrarium.heracles.common.handlers.progress.QuestsProgress; +import earth.terrarium.heracles.common.handlers.quests.CompletableQuests; import earth.terrarium.heracles.common.handlers.quests.QuestHandler; import earth.terrarium.heracles.common.menus.quest.QuestContent; import earth.terrarium.heracles.common.menus.quests.QuestsContent; @@ -117,12 +118,19 @@ public static void editGroup(ServerPlayer player, String group) { private static Map getQuests(ServerPlayer player) { Map quests = new HashMap<>(); QuestsProgress progress = QuestProgressHandler.getProgress(player.server, player.getUUID()); - for (String quest : progress.completableQuests().getQuests(progress)) { - quests.put(quest, QuestStatus.IN_PROGRESS); - } + CompletableQuests completableQuests = progress.completableQuests(); QuestHandler.quests().forEach((id, quest) -> { if (!quests.containsKey(id)) { - quests.put(id, progress.isComplete(id) ? (progress.isClaimed(id, quest) ? QuestStatus.COMPLETED_CLAIMED : QuestStatus.COMPLETED) : QuestStatus.LOCKED); + boolean complete = progress.isComplete(id); + boolean unlocked = progress.isUnlocked(id); + boolean claimed = progress.isClaimed(id, quest); + quests.put( + id, + unlocked ? + (complete ? + (claimed ? QuestStatus.COMPLETED_CLAIMED : QuestStatus.COMPLETED) : + QuestStatus.IN_PROGRESS) : + (complete ? QuestStatus.PROVISIONALLY_COMPLETED : QuestStatus.LOCKED)); } }); return quests; @@ -132,7 +140,8 @@ public enum QuestStatus implements StringRepresentable { LOCKED, IN_PROGRESS, COMPLETED, - COMPLETED_CLAIMED; + COMPLETED_CLAIMED, + PROVISIONALLY_COMPLETED; public boolean isComplete() { return this == COMPLETED || this == COMPLETED_CLAIMED; diff --git a/common/src/main/resources/assets/heracles/lang/en_us.json b/common/src/main/resources/assets/heracles/lang/en_us.json index 2b4a1dd5..c596ae13 100644 --- a/common/src/main/resources/assets/heracles/lang/en_us.json +++ b/common/src/main/resources/assets/heracles/lang/en_us.json @@ -9,9 +9,13 @@ "quest.heracles.available": "Available", "quest.heracles.claimed": "Claimed", "quest.heracles.dependencies_visible": "Parents Visible", + "quest.heracles.linear": "Linear", + "quest.heracles.flexible": "Flexible", "quest.heracles.toast": "Quest Complete!", "quest.heracles.toast.desc": "Press [%s] to open", + "quest.heracles.toast.provisional": "Quest Tasks Complete!", + "quest.heracles.toast.provisional.desc": "You can claim rewards once unlocked", "quest_unlocked.heracles.toast": "Quest Unlocked!", "quest_unlocked.heracles.toast.desc": "Press [%s] to open", @@ -29,6 +33,7 @@ "gui.heracles.pinned_quests.move": "Move Pinned Quests", "gui.heracles.pinned_quests.placeholder": "Quest %s", "gui.heracles.quest.title.complete": "✔ %s ✔", + "gui.heracles.quest.title.provisional": "⏳ %s ⏳", "gui.heracles.progress.title.incomplete": "Incomplete", "gui.heracles.progress.title.complete": "Complete", "gui.heracles.progress.desc.incomplete.singular": "%s Task Left", @@ -101,6 +106,7 @@ "setting.heracles.quest.show_dependency_arrow": "Show Dependency Arrow", "setting.heracles.quest.repeatable": "Repeatable", "setting.heracles.quest.auto_claim_rewards": "Auto Claim Rewards", + "setting.heracles.quest.progression_mode": "Progression Mode", "rei.sections.odyssey": "Project Odyssey", "rei.heracles.heracles.tooltip": "Open Quests", diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/circles.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/circles.png index 4bbb2933..0edf1f60 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/circles.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/circles.png differ diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/default.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/default.png index 94ca5440..a8cb5aac 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/default.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/default.png differ diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/diamonds.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/diamonds.png index a99538ca..1e226f43 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/diamonds.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/diamonds.png differ diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/gears.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/gears.png index 14f907c7..61b8f6ac 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/gears.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/gears.png differ diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/hearts.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/hearts.png index 4396ab8c..5a2f1400 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/hearts.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/hearts.png differ diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/hexagons.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/hexagons.png index ca49fb16..3890cb4f 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/hexagons.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/hexagons.png differ diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/octagons.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/octagons.png index 8e1f52ed..e3e99dad 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/octagons.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/octagons.png differ diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/pentagons.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/pentagons.png index 1fea36bd..f95da63b 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/pentagons.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/pentagons.png differ diff --git a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/rounded_squares.png b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/rounded_squares.png index 10aa1f7b..770659b9 100644 Binary files a/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/rounded_squares.png and b/common/src/main/resources/assets/heracles/textures/gui/quest_backgrounds/rounded_squares.png differ