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, T, ?> 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