feat(io,command): quests command, QuestText, per-turn quest tick

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:31:33 +02:00
parent a3600eadd7
commit 310d476264
6 changed files with 100 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ import thb.jeanluc.adventure.command.impl.HelpCommand;
import thb.jeanluc.adventure.command.impl.InventoryCommand;
import thb.jeanluc.adventure.command.impl.LookCommand;
import thb.jeanluc.adventure.command.impl.MapCommand;
import thb.jeanluc.adventure.command.impl.QuestsCommand;
import thb.jeanluc.adventure.command.impl.QuitCommand;
import thb.jeanluc.adventure.command.impl.ReadCommand;
import thb.jeanluc.adventure.command.impl.TakeCommand;
@@ -70,6 +71,7 @@ public final class App {
registry.register(new ReadCommand(), "read");
registry.register(new ExamineCommand(), "examine", "x", "inspect");
registry.register(new MapCommand(), "map", "m");
registry.register(new QuestsCommand(), "quests", "log", "journal");
registry.register(new TalkCommand(), "talk", "speak");
registry.register(new GiveCommand(), "give");
registry.register(new HelpCommand(registry), "help", "?");

View File

@@ -0,0 +1,21 @@
package thb.jeanluc.adventure.command.impl;
import thb.jeanluc.adventure.command.Command;
import thb.jeanluc.adventure.game.GameContext;
import thb.jeanluc.adventure.game.QuestEngine;
import java.util.List;
/** Shows the active and completed quests. Usage: {@code quests}. */
public class QuestsCommand implements Command {
@Override
public void execute(GameContext ctx, List<String> args) {
ctx.getIo().showQuests(QuestEngine.viewOf(ctx));
}
@Override
public String help() {
return "quests - show your active and completed quests (alias: log, journal)";
}
}

View File

@@ -60,6 +60,7 @@ public class Game {
}
private void publishHud() {
QuestEngine.tick(ctx);
ctx.getIo().setHud(new Hud(
ctx.getPlayer().getCurrentRoom().getName(),
ctx.getPlayer().getGold(),
@@ -70,6 +71,7 @@ public class Game {
ctx.getPlayer().getVisitedRoomIds(),
ctx.getPlayer().getCurrentRoom());
ctx.getIo().setMap(map);
ctx.getIo().setQuests(QuestEngine.viewOf(ctx));
}
/**

View File

@@ -3,6 +3,8 @@ package thb.jeanluc.adventure.io;
import thb.jeanluc.adventure.io.text.AsciiMap;
import thb.jeanluc.adventure.io.text.Hud;
import thb.jeanluc.adventure.io.text.MapView;
import thb.jeanluc.adventure.io.text.QuestText;
import thb.jeanluc.adventure.io.text.QuestView;
import thb.jeanluc.adventure.io.text.Renderings;
import thb.jeanluc.adventure.io.text.RoomView;
import thb.jeanluc.adventure.io.text.StyledText;
@@ -44,4 +46,14 @@ public interface GameIO {
default void showMap(MapView view) {
print(AsciiMap.render(view, true));
}
/** Per-turn push of the quest log. GUI repaints its box; console ignores. */
default void setQuests(QuestView view) {
// no-op
}
/** On-demand quest log (the 'quests' command). Default renders styled text. */
default void showQuests(QuestView view) {
print(QuestText.render(view));
}
}

View File

@@ -0,0 +1,29 @@
package thb.jeanluc.adventure.io.text;
/** Renders a {@link QuestView} as styled text for the console quest log. */
public final class QuestText {
private QuestText() {
}
public static StyledText render(QuestView view) {
if (view.active().isEmpty() && view.completed().isEmpty()) {
return StyledText.of("You have no quests yet.");
}
StyledText.Builder b = StyledText.builder();
b.heading("QUESTS");
if (view.active().isEmpty()) {
b.plain("\n").dim(" (none active)");
}
for (QuestEntry e : view.active()) {
b.plain("\n").heading(e.title()).plain("\n").exit(" -> ").plain(e.objective());
}
if (!view.completed().isEmpty()) {
b.plain("\n\n").dim("Completed:");
for (String t : view.completed()) {
b.plain("\n").dim("" + t);
}
}
return b.build();
}
}

View File

@@ -0,0 +1,34 @@
package thb.jeanluc.adventure.command.impl;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.game.GameContext;
import thb.jeanluc.adventure.game.QuestEngine;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.model.Condition;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Quest;
import thb.jeanluc.adventure.model.QuestStage;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.World;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class QuestsCommandTest {
@Test
void showsActiveObjective() {
Quest q = new Quest("q", "My Quest", true,
List.of(new QuestStage("Do the thing",
List.of(new Condition(Condition.Type.FLAG, "x")), List.of())), List.of());
World w = new World(Map.of(), Map.of(), Map.of(), "t", "w", Map.of("q", q));
GameContext ctx = new GameContext(w, new Player(new Room("k", "K", "d"), 0), new TestIO());
QuestEngine.tick(ctx); // auto-start
new QuestsCommand().execute(ctx, List.of());
String out = ((TestIO) ctx.getIo()).allOutput();
assertThat(out).contains("My Quest").contains("Do the thing");
}
}