feat(game): Quest model, QuestLog, START_QUEST effect
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ public final class Effects {
|
|||||||
}
|
}
|
||||||
case REMOVE_ITEM -> ctx.getPlayer().removeItem(e.arg());
|
case REMOVE_ITEM -> ctx.getPlayer().removeItem(e.arg());
|
||||||
case SAY -> ctx.getIo().write(e.arg());
|
case SAY -> ctx.getIo().write(e.arg());
|
||||||
|
case START_QUEST -> ctx.getQuestLog().start(e.arg());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,7 @@ public class GameContext {
|
|||||||
|
|
||||||
/** Mutable world flags. Created fresh per context; not a constructor arg. */
|
/** Mutable world flags. Created fresh per context; not a constructor arg. */
|
||||||
private final GameState state = new GameState();
|
private final GameState state = new GameState();
|
||||||
|
|
||||||
|
/** Runtime quest progress. Created fresh per context; not a constructor arg. */
|
||||||
|
private final QuestLog questLog = new QuestLog();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package thb.jeanluc.adventure.game;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Runtime quest progress: current stage per active quest, plus completed ids. */
|
||||||
|
public class QuestLog {
|
||||||
|
|
||||||
|
private final Map<String, Integer> stageIndex = new LinkedHashMap<>();
|
||||||
|
private final Set<String> completed = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
public void start(String id) {
|
||||||
|
if (!completed.contains(id)) {
|
||||||
|
stageIndex.putIfAbsent(id, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive(String id) {
|
||||||
|
return stageIndex.containsKey(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCompleted(String id) {
|
||||||
|
return completed.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int stageIndex(String id) {
|
||||||
|
return stageIndex.getOrDefault(id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void advance(String id) {
|
||||||
|
stageIndex.merge(id, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void complete(String id) {
|
||||||
|
stageIndex.remove(id);
|
||||||
|
completed.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> active() {
|
||||||
|
return Collections.unmodifiableSet(stageIndex.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> completed() {
|
||||||
|
return Collections.unmodifiableSet(completed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,5 +2,5 @@ package thb.jeanluc.adventure.model;
|
|||||||
|
|
||||||
/** A single state mutation or message. */
|
/** A single state mutation or message. */
|
||||||
public record Effect(Type type, String arg) {
|
public record Effect(Type type, String arg) {
|
||||||
public enum Type { SET_FLAG, CLEAR_FLAG, GIVE_ITEM, REMOVE_ITEM, SAY }
|
public enum Type { SET_FLAG, CLEAR_FLAG, GIVE_ITEM, REMOVE_ITEM, SAY, START_QUEST }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package thb.jeanluc.adventure.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** A multi-stage quest. Stage completion is condition-driven; rewards are effects. */
|
||||||
|
public record Quest(String id, String title, boolean autoStart,
|
||||||
|
List<QuestStage> stages, List<Effect> onComplete) {
|
||||||
|
public Quest {
|
||||||
|
stages = stages == null ? List.of() : List.copyOf(stages);
|
||||||
|
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package thb.jeanluc.adventure.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** One stage of a {@link Quest}: an objective, a completion condition, optional rewards. */
|
||||||
|
public record QuestStage(String objective, List<Condition> completion, List<Effect> onComplete) {
|
||||||
|
public QuestStage {
|
||||||
|
completion = completion == null ? List.of() : List.copyOf(completion);
|
||||||
|
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,4 +53,11 @@ class EffectsTest {
|
|||||||
Effects.apply(new Effect(Effect.Type.SAY, "boo"), ctx);
|
Effects.apply(new Effect(Effect.Type.SAY, "boo"), ctx);
|
||||||
assertThat(((TestIO) ctx.getIo()).allOutput()).contains("boo");
|
assertThat(((TestIO) ctx.getIo()).allOutput()).contains("boo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startQuestStartsQuestInLog() {
|
||||||
|
GameContext ctx = ctx(Map.of());
|
||||||
|
Effects.apply(new Effect(Effect.Type.START_QUEST, "restore_power"), ctx);
|
||||||
|
assertThat(ctx.getQuestLog().isActive("restore_power")).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package thb.jeanluc.adventure.game;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class QuestLogTest {
|
||||||
|
@Test
|
||||||
|
void startAdvanceComplete() {
|
||||||
|
QuestLog log = new QuestLog();
|
||||||
|
assertThat(log.isActive("q")).isFalse();
|
||||||
|
log.start("q");
|
||||||
|
assertThat(log.isActive("q")).isTrue();
|
||||||
|
assertThat(log.stageIndex("q")).isEqualTo(0);
|
||||||
|
log.advance("q");
|
||||||
|
assertThat(log.stageIndex("q")).isEqualTo(1);
|
||||||
|
log.complete("q");
|
||||||
|
assertThat(log.isActive("q")).isFalse();
|
||||||
|
assertThat(log.isCompleted("q")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startIgnoredOnceCompleted() {
|
||||||
|
QuestLog log = new QuestLog();
|
||||||
|
log.start("q");
|
||||||
|
log.complete("q");
|
||||||
|
log.start("q");
|
||||||
|
assertThat(log.isActive("q")).isFalse();
|
||||||
|
assertThat(log.isCompleted("q")).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user