docs(javadoc): document every class, method and instance variable
The assignment requires Javadoc on classes, methods and instance variables. Coverage was at roughly 45%: enum constants were undocumented across the board (0 of 37), record components — which are a record's instance variables — sat at 24%, and instance fields at 52%. GameSession was @Getter-exposed with all six fields undocumented, the exact case docs/conventions.md calls out by name. Backfilled across model, io, game, loader, command, save and menu: all enum constants, all record components (@param blocks), all instance fields, and the public/protected methods and constructors that lacked tags. 'javadoc -Xdoclint:missing' now reports zero missing-doc warnings for the public API. The 30 remaining warnings are all "use of default constructor" on Lombok-generated constructors, which would require adding code to silence.
This commit is contained in:
@@ -16,10 +16,17 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MenuCommand implements Command {
|
public class MenuCommand implements Command {
|
||||||
|
|
||||||
|
/** Persistence backend the active slot is written to before leaving. */
|
||||||
private final SaveService saves;
|
private final SaveService saves;
|
||||||
|
|
||||||
|
/** Running game whose loop is stopped; null until {@link #bind(Game)} is called. */
|
||||||
private Game game;
|
private Game game;
|
||||||
|
|
||||||
/** Binds the running game (two-phase wiring, as the registry is built first). */
|
/**
|
||||||
|
* Binds the running game (two-phase wiring, as the registry is built first).
|
||||||
|
*
|
||||||
|
* @param game the game loop to stop when this command runs
|
||||||
|
*/
|
||||||
public void bind(Game game) {
|
public void bind(Game game) {
|
||||||
this.game = game;
|
this.game = game;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.util.List;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SaveCommand implements Command {
|
public class SaveCommand implements Command {
|
||||||
|
|
||||||
|
/** Persistence backend the active slot is written to. */
|
||||||
private final SaveService saves;
|
private final SaveService saves;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ public final class Conditions {
|
|||||||
private Conditions() {
|
private Conditions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates a single condition.
|
||||||
|
*
|
||||||
|
* @param c the condition to evaluate
|
||||||
|
* @param ctx the active game context
|
||||||
|
* @return true if the condition holds
|
||||||
|
*/
|
||||||
public static boolean holds(Condition c, GameContext ctx) {
|
public static boolean holds(Condition c, GameContext ctx) {
|
||||||
return switch (c.type()) {
|
return switch (c.type()) {
|
||||||
case FLAG -> ctx.getState().isSet(c.arg());
|
case FLAG -> ctx.getState().isSet(c.arg());
|
||||||
@@ -18,7 +25,13 @@ public final class Conditions {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True iff every condition holds. A null or empty list is vacuously true. */
|
/**
|
||||||
|
* True iff every condition holds. A null or empty list is vacuously true.
|
||||||
|
*
|
||||||
|
* @param conditions the conditions to evaluate; may be null
|
||||||
|
* @param ctx the active game context
|
||||||
|
* @return true if all conditions hold
|
||||||
|
*/
|
||||||
public static boolean all(List<Condition> conditions, GameContext ctx) {
|
public static boolean all(List<Condition> conditions, GameContext ctx) {
|
||||||
if (conditions == null) {
|
if (conditions == null) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ public final class Effects {
|
|||||||
private Effects() {
|
private Effects() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a single effect. An effect referencing an unknown item is logged
|
||||||
|
* and skipped rather than failing the turn.
|
||||||
|
*
|
||||||
|
* @param e the effect to apply
|
||||||
|
* @param ctx the active game context
|
||||||
|
*/
|
||||||
public static void apply(Effect e, GameContext ctx) {
|
public static void apply(Effect e, GameContext ctx) {
|
||||||
switch (e.type()) {
|
switch (e.type()) {
|
||||||
case SET_FLAG -> ctx.getState().set(e.arg());
|
case SET_FLAG -> ctx.getState().set(e.arg());
|
||||||
@@ -31,7 +38,12 @@ public final class Effects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies each effect in order. A null list is a no-op. */
|
/**
|
||||||
|
* Applies each effect in order. A null list is a no-op.
|
||||||
|
*
|
||||||
|
* @param effects the effects to apply; may be null
|
||||||
|
* @param ctx the active game context
|
||||||
|
*/
|
||||||
public static void applyAll(List<Effect> effects, GameContext ctx) {
|
public static void applyAll(List<Effect> effects, GameContext ctx) {
|
||||||
if (effects == null) {
|
if (effects == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ public final class EndingEngine {
|
|||||||
private EndingEngine() {
|
private EndingEngine() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** First ending whose conditions hold, in list order; null if none. */
|
/**
|
||||||
|
* First ending whose conditions hold, in list order; null if none.
|
||||||
|
*
|
||||||
|
* @param ctx the active game context
|
||||||
|
* @return the triggered ending, or null if no ending's conditions hold
|
||||||
|
*/
|
||||||
public static Ending triggered(GameContext ctx) {
|
public static Ending triggered(GameContext ctx) {
|
||||||
for (Ending e : ctx.getWorld().getEndings()) {
|
for (Ending e : ctx.getWorld().getEndings()) {
|
||||||
if (Conditions.all(e.when(), ctx)) {
|
if (Conditions.all(e.when(), ctx)) {
|
||||||
@@ -19,7 +24,14 @@ public final class EndingEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builds the ending screen: title banner, text, and a run summary. */
|
/**
|
||||||
|
* Builds the ending screen: title banner, text, and a run summary.
|
||||||
|
*
|
||||||
|
* @param e the triggered ending
|
||||||
|
* @param ctx the active game context
|
||||||
|
* @param turns number of turns the run took
|
||||||
|
* @return the rendered end-of-game screen
|
||||||
|
*/
|
||||||
public static StyledText render(Ending e, GameContext ctx, int turns) {
|
public static StyledText render(Ending e, GameContext ctx, int turns) {
|
||||||
int total = ctx.getWorld().getQuests().size();
|
int total = ctx.getWorld().getQuests().size();
|
||||||
int done = ctx.getQuestLog().completed().size();
|
int done = ctx.getQuestLog().completed().size();
|
||||||
@@ -34,6 +46,7 @@ public final class EndingEngine {
|
|||||||
return b.build();
|
return b.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rank line for the summary: derived from the ending's outcome and quest completeness. */
|
||||||
private static String rank(Ending e, int total, int done) {
|
private static String rank(Ending e, int total, int done) {
|
||||||
if (e.victory() && total > 0 && done == total) {
|
if (e.victory() && total > 0 && done == total) {
|
||||||
return "Master of the Manor";
|
return "Master of the Manor";
|
||||||
|
|||||||
@@ -39,7 +39,13 @@ public final class EscortEngine {
|
|||||||
private EscortEngine() {
|
private EscortEngine() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A recruitable twin and the state that tracks their escort. */
|
/**
|
||||||
|
* A recruitable twin and the state that tracks their escort.
|
||||||
|
*
|
||||||
|
* @param npcId id of the twin's NPC in the world
|
||||||
|
* @param followingFlag flag that is set while this twin follows the player
|
||||||
|
* @param homeRoomId id of the room the twin waits in until recruited
|
||||||
|
*/
|
||||||
private record Twin(String npcId, String followingFlag, String homeRoomId) {
|
private record Twin(String npcId, String followingFlag, String homeRoomId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +93,11 @@ public final class EscortEngine {
|
|||||||
return String.join(" and ", names) + " keep close at your shoulders.";
|
return String.join(" and ", names) + " keep close at your shoulders.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reunites the brothers once both follow the player into the reunion room:
|
||||||
|
* sets {@link #REUNITED_FLAG}, grants the reward, and prints the scene. Runs
|
||||||
|
* at most once per playthrough.
|
||||||
|
*/
|
||||||
private static void maybeReunite(GameContext ctx) {
|
private static void maybeReunite(GameContext ctx) {
|
||||||
if (ctx.getState().isSet(REUNITED_FLAG)) {
|
if (ctx.getState().isSet(REUNITED_FLAG)) {
|
||||||
return;
|
return;
|
||||||
@@ -107,6 +118,7 @@ public final class EscortEngine {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adds the reward item to the inventory unless the player already carries it. */
|
||||||
private static void grantReward(GameContext ctx) {
|
private static void grantReward(GameContext ctx) {
|
||||||
Item reward = ctx.getWorld().getItems().get(REWARD_ITEM_ID);
|
Item reward = ctx.getWorld().getItems().get(REWARD_ITEM_ID);
|
||||||
if (reward != null && !ctx.getPlayer().hasItem(REWARD_ITEM_ID)) {
|
if (reward != null && !ctx.getPlayer().hasItem(REWARD_ITEM_ID)) {
|
||||||
@@ -118,6 +130,7 @@ public final class EscortEngine {
|
|||||||
return ctx.getState().isSet(twin.followingFlag());
|
return ctx.getState().isSet(twin.followingFlag());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The twin's NPC name, falling back to their id if the NPC is missing from the world. */
|
||||||
private static String displayName(GameContext ctx, Twin twin) {
|
private static String displayName(GameContext ctx, Twin twin) {
|
||||||
Npc npc = ctx.getWorld().getNpcs().get(twin.npcId());
|
Npc npc = ctx.getWorld().getNpcs().get(twin.npcId());
|
||||||
return npc != null ? npc.getName() : twin.npcId();
|
return npc != null ? npc.getName() : twin.npcId();
|
||||||
|
|||||||
@@ -11,47 +11,96 @@ import thb.jeanluc.adventure.model.World;
|
|||||||
*/
|
*/
|
||||||
public class GameContext {
|
public class GameContext {
|
||||||
|
|
||||||
|
/** The mutable state of the current playthrough. */
|
||||||
private final GameSession session;
|
private final GameSession session;
|
||||||
|
|
||||||
|
/** Channel commands read input from and write output to. */
|
||||||
private final GameIO io;
|
private final GameIO io;
|
||||||
|
|
||||||
/** Silent autosave hook, wired by the app; no-op by default (tests). */
|
/** Silent autosave hook, wired by the app; no-op by default (tests). */
|
||||||
private Runnable saveCallback = () -> { };
|
private Runnable saveCallback = () -> { };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a context over an existing session.
|
||||||
|
*
|
||||||
|
* @param session the playthrough state
|
||||||
|
* @param io the IO channel
|
||||||
|
*/
|
||||||
public GameContext(GameSession session, GameIO io) {
|
public GameContext(GameSession session, GameIO io) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.io = io;
|
this.io = io;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convenience for tests/legacy callers: wraps a fresh single-use session. */
|
/**
|
||||||
|
* Convenience for tests/legacy callers: wraps a fresh single-use session.
|
||||||
|
*
|
||||||
|
* @param world the loaded world
|
||||||
|
* @param player the player character
|
||||||
|
* @param io the IO channel
|
||||||
|
*/
|
||||||
public GameContext(World world, Player player, GameIO io) {
|
public GameContext(World world, Player player, GameIO io) {
|
||||||
this(new GameSession(world, player, "session"), io);
|
this(new GameSession(world, player, "session"), io);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the session this context was built over.
|
||||||
|
*
|
||||||
|
* @return the playthrough state backing this context
|
||||||
|
*/
|
||||||
public GameSession getSession() {
|
public GameSession getSession() {
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to the session.
|
||||||
|
*
|
||||||
|
* @return the loaded world
|
||||||
|
*/
|
||||||
public World getWorld() {
|
public World getWorld() {
|
||||||
return session.getWorld();
|
return session.getWorld();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to the session.
|
||||||
|
*
|
||||||
|
* @return the player character
|
||||||
|
*/
|
||||||
public Player getPlayer() {
|
public Player getPlayer() {
|
||||||
return session.getPlayer();
|
return session.getPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to the session.
|
||||||
|
*
|
||||||
|
* @return the world flags
|
||||||
|
*/
|
||||||
public GameState getState() {
|
public GameState getState() {
|
||||||
return session.getState();
|
return session.getState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to the session.
|
||||||
|
*
|
||||||
|
* @return the quest progress
|
||||||
|
*/
|
||||||
public QuestLog getQuestLog() {
|
public QuestLog getQuestLog() {
|
||||||
return session.getQuestLog();
|
return session.getQuestLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the channel commands read from and write to.
|
||||||
|
*
|
||||||
|
* @return the IO channel
|
||||||
|
*/
|
||||||
public GameIO getIo() {
|
public GameIO getIo() {
|
||||||
return io;
|
return io;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the silent autosave hook (called by interval/quest autosave). */
|
/**
|
||||||
|
* Sets the silent autosave hook (called by interval/quest autosave).
|
||||||
|
*
|
||||||
|
* @param saveCallback the hook to run on {@link #save()}
|
||||||
|
*/
|
||||||
public void setSaveCallback(Runnable saveCallback) {
|
public void setSaveCallback(Runnable saveCallback) {
|
||||||
this.saveCallback = saveCallback;
|
this.saveCallback = saveCallback;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,23 +13,47 @@ import thb.jeanluc.adventure.model.World;
|
|||||||
@Getter
|
@Getter
|
||||||
public class GameSession {
|
public class GameSession {
|
||||||
|
|
||||||
|
/** The loaded world this playthrough runs on. */
|
||||||
private final World world;
|
private final World world;
|
||||||
|
|
||||||
|
/** The player character, including inventory, position, and visited rooms. */
|
||||||
private final Player player;
|
private final Player player;
|
||||||
|
|
||||||
|
/** World flags set and cleared by effects and conditions. */
|
||||||
private final GameState state = new GameState();
|
private final GameState state = new GameState();
|
||||||
|
|
||||||
|
/** Quest progress: active stages and completed quests. */
|
||||||
private final QuestLog questLog = new QuestLog();
|
private final QuestLog questLog = new QuestLog();
|
||||||
|
|
||||||
|
/** Name of the save slot this session is bound to; save/load target. */
|
||||||
private final String slotName;
|
private final String slotName;
|
||||||
|
|
||||||
|
/** Number of commands executed so far; shown in the HUD and the ending summary. */
|
||||||
private int turn;
|
private int turn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a session bound to a save slot.
|
||||||
|
*
|
||||||
|
* @param world the loaded world
|
||||||
|
* @param player the player character
|
||||||
|
* @param slotName the save slot this session reads from and writes to
|
||||||
|
*/
|
||||||
public GameSession(World world, Player player, String slotName) {
|
public GameSession(World world, Player player, String slotName) {
|
||||||
this.world = world;
|
this.world = world;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.slotName = slotName;
|
this.slotName = slotName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites the turn counter (used when restoring a save).
|
||||||
|
*
|
||||||
|
* @param turn the turn count to restore
|
||||||
|
*/
|
||||||
public void setTurn(int turn) {
|
public void setTurn(int turn) {
|
||||||
this.turn = turn;
|
this.turn = turn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Advances the turn counter by one. */
|
||||||
public void incrementTurn() {
|
public void incrementTurn() {
|
||||||
this.turn++;
|
this.turn++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,25 +8,51 @@ import java.util.Set;
|
|||||||
/** Named world flags. A flag is "set" (true) when present in the set. */
|
/** Named world flags. A flag is "set" (true) when present in the set. */
|
||||||
public class GameState {
|
public class GameState {
|
||||||
|
|
||||||
|
/** Ids of the currently set flags. Insertion-ordered for stable saves. */
|
||||||
private final Set<String> flags = new LinkedHashSet<>();
|
private final Set<String> flags = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether a flag is set.
|
||||||
|
*
|
||||||
|
* @param flag the flag id
|
||||||
|
* @return true if the flag is set
|
||||||
|
*/
|
||||||
public boolean isSet(String flag) {
|
public boolean isSet(String flag) {
|
||||||
return flags.contains(flag);
|
return flags.contains(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a flag. Setting an already-set flag has no effect.
|
||||||
|
*
|
||||||
|
* @param flag the flag id
|
||||||
|
*/
|
||||||
public void set(String flag) {
|
public void set(String flag) {
|
||||||
flags.add(flag);
|
flags.add(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears a flag. Clearing an unset flag has no effect.
|
||||||
|
*
|
||||||
|
* @param flag the flag id
|
||||||
|
*/
|
||||||
public void clear(String flag) {
|
public void clear(String flag) {
|
||||||
flags.remove(flag);
|
flags.remove(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All currently set flags.
|
||||||
|
*
|
||||||
|
* @return an unmodifiable view of the set flags
|
||||||
|
*/
|
||||||
public Set<String> all() {
|
public Set<String> all() {
|
||||||
return Collections.unmodifiableSet(flags);
|
return Collections.unmodifiableSet(flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Replaces all flags with the given collection (used by load). */
|
/**
|
||||||
|
* Replaces all flags with the given collection (used by load).
|
||||||
|
*
|
||||||
|
* @param newFlags the flags to restore
|
||||||
|
*/
|
||||||
public void restore(Collection<String> newFlags) {
|
public void restore(Collection<String> newFlags) {
|
||||||
flags.clear();
|
flags.clear();
|
||||||
flags.addAll(newFlags);
|
flags.addAll(newFlags);
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ public final class QuestEngine {
|
|||||||
private QuestEngine() {
|
private QuestEngine() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-turn quest progression: starts every auto-start quest that is neither
|
||||||
|
* active nor completed, then repeatedly advances active quests whose current
|
||||||
|
* stage's completion conditions hold, announcing each finished objective and
|
||||||
|
* quest. Repeats until nothing changes, so a stage whose conditions are met
|
||||||
|
* by another stage's effects still resolves in the same turn.
|
||||||
|
*
|
||||||
|
* @param ctx the active game context
|
||||||
|
*/
|
||||||
public static void tick(GameContext ctx) {
|
public static void tick(GameContext ctx) {
|
||||||
Map<String, Quest> quests = ctx.getWorld().getQuests();
|
Map<String, Quest> quests = ctx.getWorld().getQuests();
|
||||||
for (Quest q : quests.values()) {
|
for (Quest q : quests.values()) {
|
||||||
@@ -54,6 +63,7 @@ public final class QuestEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Applies a quest's completion effects, marks it done, announces it, and autosaves. */
|
||||||
private static void finish(GameContext ctx, Quest q) {
|
private static void finish(GameContext ctx, Quest q) {
|
||||||
if (ctx.getQuestLog().isCompleted(q.id())) {
|
if (ctx.getQuestLog().isCompleted(q.id())) {
|
||||||
return;
|
return;
|
||||||
@@ -65,6 +75,13 @@ public final class QuestEngine {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the quest panel view: each active quest with its current objective,
|
||||||
|
* plus the titles of the completed quests.
|
||||||
|
*
|
||||||
|
* @param ctx the active game context
|
||||||
|
* @return the current quest view
|
||||||
|
*/
|
||||||
public static QuestView viewOf(GameContext ctx) {
|
public static QuestView viewOf(GameContext ctx) {
|
||||||
Map<String, Quest> quests = ctx.getWorld().getQuests();
|
Map<String, Quest> quests = ctx.getWorld().getQuests();
|
||||||
List<QuestEntry> active = new ArrayList<>();
|
List<QuestEntry> active = new ArrayList<>();
|
||||||
@@ -85,6 +102,7 @@ public final class QuestEngine {
|
|||||||
return new QuestView(active, completed);
|
return new QuestView(active, completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stage count across all quests; bounds the iteration guard in {@link #tick}. */
|
||||||
private static int totalStages(Map<String, Quest> quests) {
|
private static int totalStages(Map<String, Quest> quests) {
|
||||||
int total = 0;
|
int total = 0;
|
||||||
for (Quest q : quests.values()) {
|
for (Quest q : quests.values()) {
|
||||||
|
|||||||
@@ -10,45 +10,97 @@ import java.util.Set;
|
|||||||
/** Runtime quest progress: current stage per active quest, plus completed ids. */
|
/** Runtime quest progress: current stage per active quest, plus completed ids. */
|
||||||
public class QuestLog {
|
public class QuestLog {
|
||||||
|
|
||||||
|
/** Index of the current stage per active quest id. Insertion-ordered for stable saves. */
|
||||||
private final Map<String, Integer> stageIndex = new LinkedHashMap<>();
|
private final Map<String, Integer> stageIndex = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/** Ids of quests that have been completed. Insertion-ordered for stable saves. */
|
||||||
private final Set<String> completed = new LinkedHashSet<>();
|
private final Set<String> completed = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates a quest at its first stage. Already active or completed quests
|
||||||
|
* are left untouched.
|
||||||
|
*
|
||||||
|
* @param id the quest id
|
||||||
|
*/
|
||||||
public void start(String id) {
|
public void start(String id) {
|
||||||
if (!completed.contains(id)) {
|
if (!completed.contains(id)) {
|
||||||
stageIndex.putIfAbsent(id, 0);
|
stageIndex.putIfAbsent(id, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether a quest is currently active.
|
||||||
|
*
|
||||||
|
* @param id the quest id
|
||||||
|
* @return true if the quest is active
|
||||||
|
*/
|
||||||
public boolean isActive(String id) {
|
public boolean isActive(String id) {
|
||||||
return stageIndex.containsKey(id);
|
return stageIndex.containsKey(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether a quest has been completed.
|
||||||
|
*
|
||||||
|
* @param id the quest id
|
||||||
|
* @return true if the quest is completed
|
||||||
|
*/
|
||||||
public boolean isCompleted(String id) {
|
public boolean isCompleted(String id) {
|
||||||
return completed.contains(id);
|
return completed.contains(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current stage index of a quest.
|
||||||
|
*
|
||||||
|
* @param id the quest id
|
||||||
|
* @return the stage index, or 0 if the quest is not active
|
||||||
|
*/
|
||||||
public int stageIndex(String id) {
|
public int stageIndex(String id) {
|
||||||
return stageIndex.getOrDefault(id, 0);
|
return stageIndex.getOrDefault(id, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a quest on to its next stage.
|
||||||
|
*
|
||||||
|
* @param id the quest id
|
||||||
|
*/
|
||||||
public void advance(String id) {
|
public void advance(String id) {
|
||||||
stageIndex.merge(id, 1, Integer::sum);
|
stageIndex.merge(id, 1, Integer::sum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a quest completed and removes it from the active set.
|
||||||
|
*
|
||||||
|
* @param id the quest id
|
||||||
|
*/
|
||||||
public void complete(String id) {
|
public void complete(String id) {
|
||||||
stageIndex.remove(id);
|
stageIndex.remove(id);
|
||||||
completed.add(id);
|
completed.add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ids of all currently active quests.
|
||||||
|
*
|
||||||
|
* @return an unmodifiable view of the active quest ids
|
||||||
|
*/
|
||||||
public Set<String> active() {
|
public Set<String> active() {
|
||||||
return Collections.unmodifiableSet(stageIndex.keySet());
|
return Collections.unmodifiableSet(stageIndex.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ids of all completed quests.
|
||||||
|
*
|
||||||
|
* @return an unmodifiable view of the completed quest ids
|
||||||
|
*/
|
||||||
public Set<String> completed() {
|
public Set<String> completed() {
|
||||||
return Collections.unmodifiableSet(completed);
|
return Collections.unmodifiableSet(completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Replaces all progress with the given stage map and completed set (load). */
|
/**
|
||||||
|
* Replaces all progress with the given stage map and completed set (load).
|
||||||
|
*
|
||||||
|
* @param stages stage index per active quest id
|
||||||
|
* @param completedIds ids of the completed quests
|
||||||
|
*/
|
||||||
public void restore(Map<String, Integer> stages, Collection<String> completedIds) {
|
public void restore(Map<String, Integer> stages, Collection<String> completedIds) {
|
||||||
stageIndex.clear();
|
stageIndex.clear();
|
||||||
stageIndex.putAll(stages);
|
stageIndex.putAll(stages);
|
||||||
|
|||||||
@@ -18,25 +18,51 @@ import java.util.Optional;
|
|||||||
*/
|
*/
|
||||||
public final class TutorialGuide {
|
public final class TutorialGuide {
|
||||||
|
|
||||||
|
/** The walkthrough content: intro, steps, and closing tips. */
|
||||||
private final Tutorial tutorial;
|
private final Tutorial tutorial;
|
||||||
|
|
||||||
|
/** Command lookup, used to compare a typed verb against a step's expected command via aliases. */
|
||||||
private final CommandRegistry registry;
|
private final CommandRegistry registry;
|
||||||
|
|
||||||
|
/** Index of the step currently being taught. */
|
||||||
private int index;
|
private int index;
|
||||||
|
|
||||||
|
/** False once all steps are done or the walkthrough was skipped. */
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
|
||||||
|
/** True once {@link #begin(GameContext)} has printed the intro; guards against a second run. */
|
||||||
private boolean begun;
|
private boolean begun;
|
||||||
|
|
||||||
|
/** Room the player stood in when the current step began; used to detect movement steps. */
|
||||||
private Room roomAtStepStart;
|
private Room roomAtStepStart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a guide for the given walkthrough. The guide starts inactive if
|
||||||
|
* the tutorial has no steps.
|
||||||
|
*
|
||||||
|
* @param tutorial the walkthrough content
|
||||||
|
* @param registry command lookup for alias-aware verb comparison
|
||||||
|
*/
|
||||||
public TutorialGuide(Tutorial tutorial, CommandRegistry registry) {
|
public TutorialGuide(Tutorial tutorial, CommandRegistry registry) {
|
||||||
this.tutorial = tutorial;
|
this.tutorial = tutorial;
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.active = !tutorial.isEmpty();
|
this.active = !tutorial.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return true while the walkthrough is still running. */
|
/**
|
||||||
|
* Reports whether the guide still has steps left to hand out.
|
||||||
|
*
|
||||||
|
* @return true while the walkthrough is still running
|
||||||
|
*/
|
||||||
public boolean isActive() {
|
public boolean isActive() {
|
||||||
return active;
|
return active;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prints the intro and the first instruction (call once at loop start). */
|
/**
|
||||||
|
* Prints the intro and the first instruction (call once at loop start).
|
||||||
|
*
|
||||||
|
* @param ctx the active game context
|
||||||
|
*/
|
||||||
public void begin(GameContext ctx) {
|
public void begin(GameContext ctx) {
|
||||||
if (!active || begun) {
|
if (!active || begun) {
|
||||||
return;
|
return;
|
||||||
@@ -48,7 +74,14 @@ public final class TutorialGuide {
|
|||||||
enterStep(ctx);
|
enterStep(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Evaluates the current step against the command that was just executed. */
|
/**
|
||||||
|
* Evaluates the current step against the command that was just executed:
|
||||||
|
* on success prints the confirmation and advances (ending the walkthrough
|
||||||
|
* after the last step), otherwise prints the step's hint.
|
||||||
|
*
|
||||||
|
* @param ctx the active game context
|
||||||
|
* @param parsed the command that was just executed
|
||||||
|
*/
|
||||||
public void onCommand(GameContext ctx, ParsedCommand parsed) {
|
public void onCommand(GameContext ctx, ParsedCommand parsed) {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
return;
|
return;
|
||||||
@@ -70,7 +103,11 @@ public final class TutorialGuide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ends the tutorial early (the 'skip' command). */
|
/**
|
||||||
|
* Ends the tutorial early (the 'skip' command).
|
||||||
|
*
|
||||||
|
* @param ctx the active game context
|
||||||
|
*/
|
||||||
public void skip(GameContext ctx) {
|
public void skip(GameContext ctx) {
|
||||||
if (active) {
|
if (active) {
|
||||||
ctx.getIo().write("Tutorial skipped.");
|
ctx.getIo().write("Tutorial skipped.");
|
||||||
@@ -78,11 +115,18 @@ public final class TutorialGuide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Prints the current step's instruction and records the room it started in. */
|
||||||
private void enterStep(GameContext ctx) {
|
private void enterStep(GameContext ctx) {
|
||||||
roomAtStepStart = ctx.getPlayer().getCurrentRoom();
|
roomAtStepStart = ctx.getPlayer().getCurrentRoom();
|
||||||
ctx.getIo().write(tutorial.steps().get(index).instruction());
|
ctx.getIo().write(tutorial.steps().get(index).instruction());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the executed command fulfils the step. The pseudo-expectations
|
||||||
|
* {@code go_direction} and {@code go_to_room} additionally require an actual
|
||||||
|
* room change; every other expectation matches on the command (aliases
|
||||||
|
* included) and the step's minimum argument count.
|
||||||
|
*/
|
||||||
private boolean satisfies(TutorialStep step, GameContext ctx, ParsedCommand parsed) {
|
private boolean satisfies(TutorialStep step, GameContext ctx, ParsedCommand parsed) {
|
||||||
String expect = step.expect();
|
String expect = step.expect();
|
||||||
if ("go_direction".equals(expect)) {
|
if ("go_direction".equals(expect)) {
|
||||||
@@ -98,6 +142,7 @@ public final class TutorialGuide {
|
|||||||
return sameCommand(p.verb(), "go") && !p.args().isEmpty();
|
return sameCommand(p.verb(), "go") && !p.args().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the first argument parses as a {@link Direction} (rather than a room name). */
|
||||||
private boolean firstIsDirection(ParsedCommand p) {
|
private boolean firstIsDirection(ParsedCommand p) {
|
||||||
if (p.args().isEmpty()) {
|
if (p.args().isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -110,11 +155,13 @@ public final class TutorialGuide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the player left the room the current step started in. */
|
||||||
private boolean moved(GameContext ctx) {
|
private boolean moved(GameContext ctx) {
|
||||||
return roomAtStepStart != null
|
return roomAtStepStart != null
|
||||||
&& ctx.getPlayer().getCurrentRoom() != roomAtStepStart;
|
&& ctx.getPlayer().getCurrentRoom() != roomAtStepStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether both verbs resolve to the same registered command, so aliases count as a match. */
|
||||||
private boolean sameCommand(String a, String b) {
|
private boolean sameCommand(String a, String b) {
|
||||||
Optional<Command> ca = registry.find(a);
|
Optional<Command> ca = registry.find(a);
|
||||||
Optional<Command> cb = registry.find(b);
|
Optional<Command> cb = registry.find(b);
|
||||||
|
|||||||
@@ -23,28 +23,70 @@ import java.util.List;
|
|||||||
public class ConsoleIO implements GameIO {
|
public class ConsoleIO implements GameIO {
|
||||||
|
|
||||||
/** Whether ANSI colour is emitted. */
|
/** Whether ANSI colour is emitted. */
|
||||||
public enum ColorMode { ON, OFF, AUTO }
|
public enum ColorMode {
|
||||||
|
|
||||||
|
/** Always emit ANSI escape codes. */
|
||||||
|
ON,
|
||||||
|
|
||||||
|
/** Never emit ANSI escape codes; plain text only. */
|
||||||
|
OFF,
|
||||||
|
|
||||||
|
/** Emit colour only when attached to a real console, not when piped. */
|
||||||
|
AUTO
|
||||||
|
}
|
||||||
|
|
||||||
/** Frame/glyph fidelity tier. */
|
/** Frame/glyph fidelity tier. */
|
||||||
public enum GlyphMode { ASCII, UNICODE, GLYPH }
|
public enum GlyphMode {
|
||||||
|
|
||||||
|
/** Frames drawn with {@code +} and {@code -}, for terminals without Unicode. */
|
||||||
|
ASCII,
|
||||||
|
|
||||||
|
/** Frames drawn with Unicode box-drawing characters. */
|
||||||
|
UNICODE,
|
||||||
|
|
||||||
|
/** Highest tier: Unicode frames plus decorative glyphs. */
|
||||||
|
GLYPH
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Total width of a room frame, in characters. */
|
||||||
private static final int FRAME_WIDTH = 44;
|
private static final int FRAME_WIDTH = 44;
|
||||||
|
|
||||||
|
/** Source the player's commands are read from. */
|
||||||
private final BufferedReader in;
|
private final BufferedReader in;
|
||||||
|
|
||||||
|
/** Sink all output is written to. */
|
||||||
private final PrintStream out;
|
private final PrintStream out;
|
||||||
|
|
||||||
|
/** Whether ANSI colour codes are currently emitted; resolved from a {@link ColorMode}. */
|
||||||
private boolean useColor;
|
private boolean useColor;
|
||||||
|
|
||||||
|
/** Frame/glyph fidelity tier currently in use. */
|
||||||
private GlyphMode glyphs;
|
private GlyphMode glyphs;
|
||||||
|
|
||||||
|
/** Reads from {@code System.in} and writes to {@code System.out}, colour auto-detected. */
|
||||||
public ConsoleIO() {
|
public ConsoleIO() {
|
||||||
this(new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)),
|
this(new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)),
|
||||||
System.out, ColorMode.AUTO, GlyphMode.UNICODE);
|
System.out, ColorMode.AUTO, GlyphMode.UNICODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Backward-compatible stream constructor (colour off, unicode frames). */
|
/**
|
||||||
|
* Backward-compatible stream constructor (colour off, unicode frames).
|
||||||
|
*
|
||||||
|
* @param in source of the player's commands
|
||||||
|
* @param out sink for all output
|
||||||
|
*/
|
||||||
public ConsoleIO(BufferedReader in, PrintStream out) {
|
public ConsoleIO(BufferedReader in, PrintStream out) {
|
||||||
this(in, out, ColorMode.OFF, GlyphMode.UNICODE);
|
this(in, out, ColorMode.OFF, GlyphMode.UNICODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a console IO bound to the given streams and fidelity settings.
|
||||||
|
*
|
||||||
|
* @param in source of the player's commands
|
||||||
|
* @param out sink for all output
|
||||||
|
* @param colorMode whether ANSI colour is emitted
|
||||||
|
* @param glyphs frame/glyph fidelity tier
|
||||||
|
*/
|
||||||
public ConsoleIO(BufferedReader in, PrintStream out, ColorMode colorMode, GlyphMode glyphs) {
|
public ConsoleIO(BufferedReader in, PrintStream out, ColorMode colorMode, GlyphMode glyphs) {
|
||||||
this.in = in;
|
this.in = in;
|
||||||
this.out = out;
|
this.out = out;
|
||||||
@@ -52,7 +94,11 @@ public class ConsoleIO implements GameIO {
|
|||||||
setColorMode(colorMode);
|
setColorMode(colorMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Re-resolves colour emission from the given mode (AUTO checks the console). */
|
/**
|
||||||
|
* Re-resolves colour emission from the given mode (AUTO checks the console).
|
||||||
|
*
|
||||||
|
* @param mode the colour mode to apply
|
||||||
|
*/
|
||||||
public void setColorMode(ColorMode mode) {
|
public void setColorMode(ColorMode mode) {
|
||||||
this.useColor = switch (mode) {
|
this.useColor = switch (mode) {
|
||||||
case ON -> true;
|
case ON -> true;
|
||||||
@@ -61,7 +107,11 @@ public class ConsoleIO implements GameIO {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Switches the frame/glyph fidelity tier live. */
|
/**
|
||||||
|
* Switches the frame/glyph fidelity tier live.
|
||||||
|
*
|
||||||
|
* @param mode the tier to apply
|
||||||
|
*/
|
||||||
public void setGlyphMode(GlyphMode mode) {
|
public void setGlyphMode(GlyphMode mode) {
|
||||||
this.glyphs = mode;
|
this.glyphs = mode;
|
||||||
}
|
}
|
||||||
@@ -118,6 +168,14 @@ public class ConsoleIO implements GameIO {
|
|||||||
print(AsciiMap.render(view, glyphs != GlyphMode.ASCII));
|
print(AsciiMap.render(view, glyphs != GlyphMode.ASCII));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds one indented, labelled line listing the given entries.
|
||||||
|
*
|
||||||
|
* @param label dim heading in front of the entries
|
||||||
|
* @param xs the entries
|
||||||
|
* @param st style applied to each entry
|
||||||
|
* @return the rendered line
|
||||||
|
*/
|
||||||
private String section(String label, List<String> xs, Style st) {
|
private String section(String label, List<String> xs, Style st) {
|
||||||
StringBuilder sb = new StringBuilder(" ").append(paint(Style.DIM, label)).append(" ");
|
StringBuilder sb = new StringBuilder(" ").append(paint(Style.DIM, label)).append(" ");
|
||||||
for (int i = 0; i < xs.size(); i++) {
|
for (int i = 0; i < xs.size(); i++) {
|
||||||
@@ -129,6 +187,10 @@ public class ConsoleIO implements GameIO {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param title room name embedded in the top border
|
||||||
|
* @return the top border of a room frame, {@link #FRAME_WIDTH} characters wide
|
||||||
|
*/
|
||||||
private String frameTop(String title) {
|
private String frameTop(String title) {
|
||||||
String tl = glyphs == GlyphMode.ASCII ? "+" : "┌";
|
String tl = glyphs == GlyphMode.ASCII ? "+" : "┌";
|
||||||
String tr = glyphs == GlyphMode.ASCII ? "+" : "┐";
|
String tr = glyphs == GlyphMode.ASCII ? "+" : "┐";
|
||||||
@@ -141,6 +203,9 @@ public class ConsoleIO implements GameIO {
|
|||||||
return sb.append(tr).toString();
|
return sb.append(tr).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the bottom border of a room frame, {@link #FRAME_WIDTH} characters wide
|
||||||
|
*/
|
||||||
private String frameBottom() {
|
private String frameBottom() {
|
||||||
String bl = glyphs == GlyphMode.ASCII ? "+" : "└";
|
String bl = glyphs == GlyphMode.ASCII ? "+" : "└";
|
||||||
String br = glyphs == GlyphMode.ASCII ? "+" : "┘";
|
String br = glyphs == GlyphMode.ASCII ? "+" : "┘";
|
||||||
@@ -152,6 +217,13 @@ public class ConsoleIO implements GameIO {
|
|||||||
return sb.append(br).toString();
|
return sb.append(br).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the text in the ANSI escape codes of the given role.
|
||||||
|
*
|
||||||
|
* @param st the semantic role
|
||||||
|
* @param s the text
|
||||||
|
* @return the text unchanged when colour is disabled, else the coloured text
|
||||||
|
*/
|
||||||
private String paint(Style st, String s) {
|
private String paint(Style st, String s) {
|
||||||
if (!useColor) {
|
if (!useColor) {
|
||||||
return s;
|
return s;
|
||||||
|
|||||||
@@ -18,28 +18,52 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public interface GameIO {
|
public interface GameIO {
|
||||||
|
|
||||||
/** Blocks until a line of input is available; empty string at end of input. */
|
/**
|
||||||
|
* Blocks until a line of input is available; empty string at end of input.
|
||||||
|
*
|
||||||
|
* @return the line the player entered
|
||||||
|
*/
|
||||||
String readLine();
|
String readLine();
|
||||||
|
|
||||||
/** Renders one styled output block. The only method implementors must provide. */
|
/**
|
||||||
|
* Renders one styled output block. The only method implementors must provide.
|
||||||
|
*
|
||||||
|
* @param text the block to render
|
||||||
|
*/
|
||||||
void print(StyledText text);
|
void print(StyledText text);
|
||||||
|
|
||||||
/** Convenience: a plain, unstyled line. */
|
/**
|
||||||
|
* Convenience: a plain, unstyled line.
|
||||||
|
*
|
||||||
|
* @param s the text to write
|
||||||
|
*/
|
||||||
default void write(String s) {
|
default void write(String s) {
|
||||||
print(StyledText.of(s));
|
print(StyledText.of(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renders a room. Default flattens via {@link Renderings}; renderers may override. */
|
/**
|
||||||
|
* Renders a room. Default flattens via {@link Renderings}; renderers may override.
|
||||||
|
*
|
||||||
|
* @param room the room snapshot to render
|
||||||
|
*/
|
||||||
default void showRoom(RoomView room) {
|
default void showRoom(RoomView room) {
|
||||||
print(Renderings.roomToStyledText(room));
|
print(Renderings.roomToStyledText(room));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the status/HUD region. No-op by default; renderers may override. */
|
/**
|
||||||
|
* Updates the status/HUD region. No-op by default; renderers may override.
|
||||||
|
*
|
||||||
|
* @param hud the status snapshot
|
||||||
|
*/
|
||||||
default void setHud(Hud hud) {
|
default void setHud(Hud hud) {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Per-turn push of the current map. GUI repaints its panel; console ignores. */
|
/**
|
||||||
|
* Per-turn push of the current map. GUI repaints its panel; console ignores.
|
||||||
|
*
|
||||||
|
* @param view the map snapshot
|
||||||
|
*/
|
||||||
default void setMap(MapView view) {
|
default void setMap(MapView view) {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
@@ -55,27 +79,47 @@ public interface GameIO {
|
|||||||
// no animation in text mode
|
// no animation in text mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/** On-demand map (the 'map' command). Default renders ASCII (Unicode frames). */
|
/**
|
||||||
|
* On-demand map (the 'map' command). Default renders ASCII (Unicode frames).
|
||||||
|
*
|
||||||
|
* @param view the map snapshot
|
||||||
|
*/
|
||||||
default void showMap(MapView view) {
|
default void showMap(MapView view) {
|
||||||
print(AsciiMap.render(view, true));
|
print(AsciiMap.render(view, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Per-turn push of the quest log. GUI repaints its box; console ignores. */
|
/**
|
||||||
|
* Per-turn push of the quest log. GUI repaints its box; console ignores.
|
||||||
|
*
|
||||||
|
* @param view the quest snapshot
|
||||||
|
*/
|
||||||
default void setQuests(QuestView view) {
|
default void setQuests(QuestView view) {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Per-turn current-room music track; null/blank = silence. Console no-op; GUI plays. */
|
/**
|
||||||
|
* Per-turn current-room music track; null/blank = silence. Console no-op; GUI plays.
|
||||||
|
*
|
||||||
|
* @param track the current room's track name; null or blank means silence
|
||||||
|
*/
|
||||||
default void setMusic(String track) {
|
default void setMusic(String track) {
|
||||||
// no audio in text mode
|
// no audio in text mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies the music volume/level. Console no-op; GUI sets it. */
|
/**
|
||||||
|
* Applies the music volume/level. Console no-op; GUI sets it.
|
||||||
|
*
|
||||||
|
* @param level the level to apply
|
||||||
|
*/
|
||||||
default void setMusicLevel(MusicLevel level) {
|
default void setMusicLevel(MusicLevel level) {
|
||||||
// no audio in text mode
|
// no audio in text mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/** On-demand quest log (the 'quests' command). Default renders styled text. */
|
/**
|
||||||
|
* On-demand quest log (the 'quests' command). Default renders styled text.
|
||||||
|
*
|
||||||
|
* @param view the quest snapshot
|
||||||
|
*/
|
||||||
default void showQuests(QuestView view) {
|
default void showQuests(QuestView view) {
|
||||||
print(QuestText.render(view));
|
print(QuestText.render(view));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,30 +31,64 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class MapPanel extends JComponent {
|
public class MapPanel extends JComponent {
|
||||||
|
|
||||||
|
/** Unscaled room width, in pixels. */
|
||||||
private static final int BASE_CELL_W = 96;
|
private static final int BASE_CELL_W = 96;
|
||||||
|
|
||||||
|
/** Unscaled room height, in pixels. */
|
||||||
private static final int BASE_CELL_H = 46;
|
private static final int BASE_CELL_H = 46;
|
||||||
|
|
||||||
|
/** Unscaled horizontal gap between two rooms, in pixels. */
|
||||||
private static final int BASE_GAP_X = 34;
|
private static final int BASE_GAP_X = 34;
|
||||||
|
|
||||||
|
/** Unscaled vertical gap between two rooms, in pixels. */
|
||||||
private static final int BASE_GAP_Y = 30;
|
private static final int BASE_GAP_Y = 30;
|
||||||
|
|
||||||
|
/** Unscaled padding around the whole map, in pixels. */
|
||||||
private static final int BASE_PAD = 16;
|
private static final int BASE_PAD = 16;
|
||||||
|
|
||||||
|
/** Unscaled size of the room-label font, in points. */
|
||||||
private static final float BASE_FONT = 11f;
|
private static final float BASE_FONT = 11f;
|
||||||
|
|
||||||
|
/** Panel background. */
|
||||||
private static final Color BG = new Color(0x0e, 0x12, 0x18);
|
private static final Color BG = new Color(0x0e, 0x12, 0x18);
|
||||||
|
|
||||||
|
/** Line colour of a corridor between two visited rooms. */
|
||||||
private static final Color CORRIDOR = new Color(0x46, 0x70, 0x8a);
|
private static final Color CORRIDOR = new Color(0x46, 0x70, 0x8a);
|
||||||
|
|
||||||
|
/** Outline colour of a {@link CellState#VISITED} room. */
|
||||||
private static final Color VISITED = new Color(0xcf, 0xd6, 0xe0);
|
private static final Color VISITED = new Color(0xcf, 0xd6, 0xe0);
|
||||||
|
|
||||||
|
/** Outline colour of the {@link CellState#CURRENT} room. */
|
||||||
private static final Color CURRENT = new Color(0xe6, 0xc3, 0x4a);
|
private static final Color CURRENT = new Color(0xe6, 0xc3, 0x4a);
|
||||||
|
|
||||||
|
/** Outline colour of a {@link CellState#KNOWN} but unvisited room. */
|
||||||
private static final Color KNOWN = new Color(0x3a, 0x46, 0x55);
|
private static final Color KNOWN = new Color(0x3a, 0x46, 0x55);
|
||||||
|
|
||||||
|
/** Font the room labels are derived from. */
|
||||||
private final Font baseFont;
|
private final Font baseFont;
|
||||||
|
|
||||||
|
/** User zoom multiplier, applied on top of the fit-to-panel scale. */
|
||||||
private float zoom = 1f;
|
private float zoom = 1f;
|
||||||
|
|
||||||
|
/** The map currently drawn; empty until the first {@link #show(MapView)}. */
|
||||||
private MapView view = new MapView(List.of(), List.of());
|
private MapView view = new MapView(List.of(), List.of());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty map panel; a map appears with the first {@link #show(MapView)}.
|
||||||
|
*
|
||||||
|
* @param font font the room labels are derived from
|
||||||
|
*/
|
||||||
public MapPanel(Font font) {
|
public MapPanel(Font font) {
|
||||||
this.baseFont = font;
|
this.baseFont = font;
|
||||||
setBackground(BG);
|
setBackground(BG);
|
||||||
setOpaque(true);
|
setOpaque(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the user zoom multiplier (applied on top of the fit-to-panel scale). */
|
/**
|
||||||
|
* Sets the user zoom multiplier (applied on top of the fit-to-panel scale).
|
||||||
|
*
|
||||||
|
* @param zoom the multiplier; clamped to a minimum of 0.4
|
||||||
|
*/
|
||||||
public void setZoom(float zoom) {
|
public void setZoom(float zoom) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
this.zoom = Math.max(0.4f, zoom);
|
this.zoom = Math.max(0.4f, zoom);
|
||||||
@@ -63,7 +97,11 @@ public class MapPanel extends JComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Replaces the rendered map. */
|
/**
|
||||||
|
* Replaces the rendered map.
|
||||||
|
*
|
||||||
|
* @param v the new map snapshot
|
||||||
|
*/
|
||||||
public void show(MapView v) {
|
public void show(MapView v) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
this.view = v;
|
this.view = v;
|
||||||
@@ -72,7 +110,11 @@ public class MapPanel extends JComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The space the map should fit into: the enclosing scroll pane's viewport area. */
|
/**
|
||||||
|
* The space the map should fit into: the enclosing scroll pane's viewport area.
|
||||||
|
*
|
||||||
|
* @return the viewport area, this panel's own size, or a fixed fallback
|
||||||
|
*/
|
||||||
private Dimension available() {
|
private Dimension available() {
|
||||||
Container p = getParent();
|
Container p = getParent();
|
||||||
if (p instanceof JViewport && p.getParent() instanceof JScrollPane sp) {
|
if (p instanceof JViewport && p.getParent() instanceof JScrollPane sp) {
|
||||||
@@ -87,6 +129,10 @@ public class MapPanel extends JComponent {
|
|||||||
return (s.width > 0 && s.height > 0) ? s : new Dimension(220, 320);
|
return (s.width > 0 && s.height > 0) ? s : new Dimension(220, 320);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the highest occupied column and row of the map grid, as
|
||||||
|
* {@code [maxX, maxY]}
|
||||||
|
*/
|
||||||
private int[] gridBounds() {
|
private int[] gridBounds() {
|
||||||
int maxX = 0;
|
int maxX = 0;
|
||||||
int maxY = 0;
|
int maxY = 0;
|
||||||
@@ -97,7 +143,13 @@ public class MapPanel extends JComponent {
|
|||||||
return new int[]{maxX, maxY};
|
return new int[]{maxX, maxY};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fit-to-panel scale times the user zoom. */
|
/**
|
||||||
|
* Fit-to-panel scale times the user zoom.
|
||||||
|
*
|
||||||
|
* @param avail the space available to the map
|
||||||
|
* @param b grid bounds as returned by {@link #gridBounds()}
|
||||||
|
* @return the effective drawing scale
|
||||||
|
*/
|
||||||
private float effScale(Dimension avail, int[] b) {
|
private float effScale(Dimension avail, int[] b) {
|
||||||
double natW = 2.0 * BASE_PAD + (b[0] + 1) * BASE_CELL_W + b[0] * BASE_GAP_X;
|
double natW = 2.0 * BASE_PAD + (b[0] + 1) * BASE_CELL_W + b[0] * BASE_GAP_X;
|
||||||
double natH = 2.0 * BASE_PAD + (b[1] + 1) * BASE_CELL_H + b[1] * BASE_GAP_Y;
|
double natH = 2.0 * BASE_PAD + (b[1] + 1) * BASE_CELL_H + b[1] * BASE_GAP_Y;
|
||||||
@@ -109,6 +161,11 @@ public class MapPanel extends JComponent {
|
|||||||
return (float) (fit * zoom);
|
return (float) (fit * zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param eff the effective drawing scale
|
||||||
|
* @param b grid bounds as returned by {@link #gridBounds()}
|
||||||
|
* @return the size of the drawn map at that scale, including padding
|
||||||
|
*/
|
||||||
private Dimension content(float eff, int[] b) {
|
private Dimension content(float eff, int[] b) {
|
||||||
int pad = Math.round(BASE_PAD * eff);
|
int pad = Math.round(BASE_PAD * eff);
|
||||||
int cw = Math.round(BASE_CELL_W * eff);
|
int cw = Math.round(BASE_CELL_W * eff);
|
||||||
|
|||||||
@@ -3,13 +3,22 @@ package thb.jeanluc.adventure.io;
|
|||||||
/** Raw audio operations — the only part that touches sound hardware. */
|
/** Raw audio operations — the only part that touches sound hardware. */
|
||||||
public interface MusicBackend {
|
public interface MusicBackend {
|
||||||
|
|
||||||
/** Fade the current track out, then start {@code track} (looping) and fade in to {@code gainDb}. */
|
/**
|
||||||
|
* Fade the current track out, then start {@code track} (looping) and fade in to {@code gainDb}.
|
||||||
|
*
|
||||||
|
* @param track name of the track to play
|
||||||
|
* @param gainDb target volume in decibels
|
||||||
|
*/
|
||||||
void fadeTo(String track, float gainDb);
|
void fadeTo(String track, float gainDb);
|
||||||
|
|
||||||
/** Fade the current track out and stop. */
|
/** Fade the current track out and stop. */
|
||||||
void fadeOut();
|
void fadeOut();
|
||||||
|
|
||||||
/** Set the playback volume immediately. */
|
/**
|
||||||
|
* Set the playback volume immediately.
|
||||||
|
*
|
||||||
|
* @param gainDb target volume in decibels
|
||||||
|
*/
|
||||||
void setGainDb(float gainDb);
|
void setGainDb(float gainDb);
|
||||||
|
|
||||||
/** Stop playback and release resources. */
|
/** Stop playback and release resources. */
|
||||||
|
|||||||
@@ -10,15 +10,31 @@ import java.util.Objects;
|
|||||||
*/
|
*/
|
||||||
public final class MusicController {
|
public final class MusicController {
|
||||||
|
|
||||||
|
/** Backend the raw playback operations are delegated to. */
|
||||||
private final MusicBackend backend;
|
private final MusicBackend backend;
|
||||||
|
|
||||||
|
/** Volume level currently selected in the Settings menu. */
|
||||||
private MusicLevel level = MusicLevel.MEDIUM;
|
private MusicLevel level = MusicLevel.MEDIUM;
|
||||||
|
|
||||||
|
/** Track currently selected, or null when silent. */
|
||||||
private String current; // currently selected track, or null = silence
|
private String current; // currently selected track, or null = silence
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a controller starting silent at {@link MusicLevel#MEDIUM}.
|
||||||
|
*
|
||||||
|
* @param backend backend that performs the actual playback
|
||||||
|
*/
|
||||||
public MusicController(MusicBackend backend) {
|
public MusicController(MusicBackend backend) {
|
||||||
this.backend = backend;
|
this.backend = backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called per turn with the current room's music field (may be null/blank). */
|
/**
|
||||||
|
* Called per turn with the current room's music field (may be null/blank).
|
||||||
|
* Does nothing while the level is {@link MusicLevel#OFF} or the track is
|
||||||
|
* already playing.
|
||||||
|
*
|
||||||
|
* @param track the room's track name; null or blank means silence
|
||||||
|
*/
|
||||||
public void room(String track) {
|
public void room(String track) {
|
||||||
if (!level.isOn()) {
|
if (!level.isOn()) {
|
||||||
return;
|
return;
|
||||||
@@ -35,7 +51,13 @@ public final class MusicController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies a new music level (volume / off). */
|
/**
|
||||||
|
* Applies a new music level (volume / off). Switching to
|
||||||
|
* {@link MusicLevel#OFF} fades out; switching back on replays on the next
|
||||||
|
* {@link #room(String)} call.
|
||||||
|
*
|
||||||
|
* @param newLevel the level to apply
|
||||||
|
*/
|
||||||
public void level(MusicLevel newLevel) {
|
public void level(MusicLevel newLevel) {
|
||||||
if (newLevel == this.level) {
|
if (newLevel == this.level) {
|
||||||
return;
|
return;
|
||||||
@@ -49,6 +71,7 @@ public final class MusicController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stops playback and releases the backend's resources. */
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
backend.shutdown();
|
backend.shutdown();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,23 @@ package thb.jeanluc.adventure.io;
|
|||||||
|
|
||||||
/** Background-music volume level, cycled in the Settings menu. */
|
/** Background-music volume level, cycled in the Settings menu. */
|
||||||
public enum MusicLevel {
|
public enum MusicLevel {
|
||||||
OFF, LOW, MEDIUM, HIGH;
|
/** Music disabled; nothing is played. */
|
||||||
|
OFF,
|
||||||
|
|
||||||
/** Target MASTER_GAIN in decibels. Irrelevant for {@link #OFF} (playback disabled). */
|
/** Quiet background music. */
|
||||||
|
LOW,
|
||||||
|
|
||||||
|
/** Default volume. */
|
||||||
|
MEDIUM,
|
||||||
|
|
||||||
|
/** Full volume, without attenuation. */
|
||||||
|
HIGH;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target MASTER_GAIN in decibels. Irrelevant for {@link #OFF} (playback disabled).
|
||||||
|
*
|
||||||
|
* @return the gain of this level
|
||||||
|
*/
|
||||||
public float gainDb() {
|
public float gainDb() {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
case OFF -> Float.NEGATIVE_INFINITY;
|
case OFF -> Float.NEGATIVE_INFINITY;
|
||||||
@@ -14,12 +28,20 @@ public enum MusicLevel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return true when music should play. */
|
/**
|
||||||
|
* Reports whether this level permits playback.
|
||||||
|
*
|
||||||
|
* @return true when music should play
|
||||||
|
*/
|
||||||
public boolean isOn() {
|
public boolean isOn() {
|
||||||
return this != OFF;
|
return this != OFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Next level in the cycle (wraps HIGH → OFF). */
|
/**
|
||||||
|
* Next level in the cycle (wraps HIGH → OFF).
|
||||||
|
*
|
||||||
|
* @return the following level
|
||||||
|
*/
|
||||||
public MusicLevel next() {
|
public MusicLevel next() {
|
||||||
return values()[(ordinal() + 1) % values().length];
|
return values()[(ordinal() + 1) % values().length];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import java.awt.Insets;
|
|||||||
/** Read-only styled view of the quest log, shown under the map. */
|
/** Read-only styled view of the quest log, shown under the map. */
|
||||||
public class QuestPanel extends JTextPane {
|
public class QuestPanel extends JTextPane {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty, read-only panel; content arrives via {@link #show(QuestView)}.
|
||||||
|
*
|
||||||
|
* @param font font the panel text is derived from
|
||||||
|
*/
|
||||||
public QuestPanel(Font font) {
|
public QuestPanel(Font font) {
|
||||||
setEditable(false);
|
setEditable(false);
|
||||||
setBackground(new Color(0x0b, 0x0e, 0x13));
|
setBackground(new Color(0x0b, 0x0e, 0x13));
|
||||||
@@ -26,6 +31,11 @@ public class QuestPanel extends JTextPane {
|
|||||||
setMargin(new Insets(8, 10, 8, 10));
|
setMargin(new Insets(8, 10, 8, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the panel's content with the given quest log.
|
||||||
|
*
|
||||||
|
* @param view the quest snapshot to render
|
||||||
|
*/
|
||||||
public void show(QuestView view) {
|
public void show(QuestView view) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
StyledDocument doc = getStyledDocument();
|
StyledDocument doc = getStyledDocument();
|
||||||
@@ -46,6 +56,10 @@ public class QuestPanel extends JTextPane {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param s the semantic role
|
||||||
|
* @return the colour this panel paints that role in
|
||||||
|
*/
|
||||||
private Color colorFor(Style s) {
|
private Color colorFor(Style s) {
|
||||||
return switch (s) {
|
return switch (s) {
|
||||||
case HEADING -> new Color(0xc9, 0x8a, 0xe0);
|
case HEADING -> new Color(0xc9, 0x8a, 0xe0);
|
||||||
|
|||||||
@@ -11,13 +11,26 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
public final class AsciiMap {
|
public final class AsciiMap {
|
||||||
|
|
||||||
|
/** Lower bound for the room-name field width, in characters. */
|
||||||
private static final int MIN_INNER = 7; // minimum room name field width
|
private static final int MIN_INNER = 7; // minimum room name field width
|
||||||
|
|
||||||
|
/** Upper bound for the room-name field width, in characters. */
|
||||||
private static final int MAX_INNER = 14; // cap so the map stays compact
|
private static final int MAX_INNER = 14; // cap so the map stays compact
|
||||||
|
|
||||||
|
/** Width of a horizontal corridor, in characters. */
|
||||||
private static final int GAP = 3; // horizontal corridor width
|
private static final int GAP = 3; // horizontal corridor width
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private AsciiMap() {
|
private AsciiMap() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the map as a styled grid of boxed rooms and corridor lines.
|
||||||
|
*
|
||||||
|
* @param view the map snapshot to draw
|
||||||
|
* @param unicode true for box-drawing characters, false for pure ASCII
|
||||||
|
* @return the rendered grid, or a placeholder when the map is empty
|
||||||
|
*/
|
||||||
public static StyledText render(MapView view, boolean unicode) {
|
public static StyledText render(MapView view, boolean unicode) {
|
||||||
if (view.cells().isEmpty()) {
|
if (view.cells().isEmpty()) {
|
||||||
return StyledText.of("(no map yet)");
|
return StyledText.of("(no map yet)");
|
||||||
@@ -98,6 +111,13 @@ public final class AsciiMap {
|
|||||||
return b.build();
|
return b.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends the text in the style that matches the cell's fog-of-war state.
|
||||||
|
*
|
||||||
|
* @param b the builder to append to
|
||||||
|
* @param s the text
|
||||||
|
* @param state the cell's state
|
||||||
|
*/
|
||||||
private static void styled(StyledText.Builder b, String s, CellState state) {
|
private static void styled(StyledText.Builder b, String s, CellState state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case CURRENT -> b.heading(s);
|
case CURRENT -> b.heading(s);
|
||||||
@@ -106,6 +126,11 @@ public final class AsciiMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param c the cell
|
||||||
|
* @param inner width of the name field, in characters
|
||||||
|
* @return the room name, truncated to {@code inner}
|
||||||
|
*/
|
||||||
private static String label(RoomCell c, int inner) {
|
private static String label(RoomCell c, int inner) {
|
||||||
String n = c.name();
|
String n = c.name();
|
||||||
if (n.length() > inner) {
|
if (n.length() > inner) {
|
||||||
@@ -114,6 +139,11 @@ public final class AsciiMap {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param s the text
|
||||||
|
* @param width target width, at least the text's length
|
||||||
|
* @return the text padded with spaces so it sits centred in {@code width}
|
||||||
|
*/
|
||||||
private static String center(String s, int width) {
|
private static String center(String s, int width) {
|
||||||
int total = width - s.length();
|
int total = width - s.length();
|
||||||
int left = total / 2;
|
int left = total / 2;
|
||||||
@@ -121,6 +151,11 @@ public final class AsciiMap {
|
|||||||
return " ".repeat(left) + s + " ".repeat(right);
|
return " ".repeat(left) + s + " ".repeat(right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param x column on the grid
|
||||||
|
* @param y row on the grid
|
||||||
|
* @return the two coordinates packed into one map/set key
|
||||||
|
*/
|
||||||
private static long key(int x, int y) {
|
private static long key(int x, int y) {
|
||||||
return (((long) x) << 32) ^ (y & 0xffffffffL);
|
return (((long) x) << 32) ^ (y & 0xffffffffL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,16 @@ package thb.jeanluc.adventure.io.text;
|
|||||||
/** Builds styled banners for big moments (game start, finale). */
|
/** Builds styled banners for big moments (game start, finale). */
|
||||||
public final class Banner {
|
public final class Banner {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private Banner() {
|
private Banner() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A framed, heading-styled welcome banner for the given title. */
|
/**
|
||||||
|
* A framed, heading-styled welcome banner for the given title.
|
||||||
|
*
|
||||||
|
* @param title the game title to frame
|
||||||
|
* @return the banner
|
||||||
|
*/
|
||||||
public static StyledText welcome(String title) {
|
public static StyledText welcome(String title) {
|
||||||
String bar = "=".repeat(Math.max(8, title.length() + 8));
|
String bar = "=".repeat(Math.max(8, title.length() + 8));
|
||||||
return StyledText.builder()
|
return StyledText.builder()
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
package thb.jeanluc.adventure.io.text;
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
/** Fog-of-war state of a room on the map. */
|
/** Fog-of-war state of a room on the map. */
|
||||||
public enum CellState { CURRENT, VISITED, KNOWN }
|
public enum CellState {
|
||||||
|
|
||||||
|
/** The room the player is standing in. */
|
||||||
|
CURRENT,
|
||||||
|
|
||||||
|
/** A room the player has already entered. */
|
||||||
|
VISITED,
|
||||||
|
|
||||||
|
/** A room that is only known as the neighbour of a visited room, never entered. */
|
||||||
|
KNOWN
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
package thb.jeanluc.adventure.io.text;
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
/** A corridor between two placed rooms. */
|
/**
|
||||||
|
* A corridor between two placed rooms.
|
||||||
|
*
|
||||||
|
* @param from cell the corridor starts at
|
||||||
|
* @param to cell the corridor leads to
|
||||||
|
*/
|
||||||
public record Connection(RoomCell from, RoomCell to) {}
|
public record Connection(RoomCell from, RoomCell to) {}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
package thb.jeanluc.adventure.io.text;
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
/** Status snapshot for the HUD region. */
|
/**
|
||||||
|
* Status snapshot for the HUD region.
|
||||||
|
*
|
||||||
|
* @param location display name of the room the player is in
|
||||||
|
* @param gold gold currently carried by the player
|
||||||
|
* @param turn number of turns played so far
|
||||||
|
* @param lightOn whether the player carries an active light source
|
||||||
|
*/
|
||||||
public record Hud(String location, int gold, int turn, boolean lightOn) {}
|
public record Hud(String location, int gold, int turn, boolean lightOn) {}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.io.text;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Frontend-agnostic snapshot of the visible map: placed cells + connections. */
|
/**
|
||||||
|
* Frontend-agnostic snapshot of the visible map: placed cells + connections.
|
||||||
|
*
|
||||||
|
* @param cells the rooms the player knows about, placed on the grid
|
||||||
|
* @param connections the corridors between those rooms
|
||||||
|
*/
|
||||||
public record MapView(List<RoomCell> cells, List<Connection> connections) {
|
public record MapView(List<RoomCell> cells, List<Connection> connections) {
|
||||||
|
/** Defensively copies both lists, making the snapshot immutable. */
|
||||||
public MapView {
|
public MapView {
|
||||||
cells = List.copyOf(cells);
|
cells = List.copyOf(cells);
|
||||||
connections = List.copyOf(connections);
|
connections = List.copyOf(connections);
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
package thb.jeanluc.adventure.io.text;
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
/** An active quest's title and its current objective. */
|
/**
|
||||||
|
* An active quest's title and its current objective.
|
||||||
|
*
|
||||||
|
* @param title display name of the quest
|
||||||
|
* @param objective description of the step the player has to complete next
|
||||||
|
*/
|
||||||
public record QuestEntry(String title, String objective) {}
|
public record QuestEntry(String title, String objective) {}
|
||||||
|
|||||||
@@ -3,9 +3,17 @@ package thb.jeanluc.adventure.io.text;
|
|||||||
/** Renders a {@link QuestView} as styled text for the console quest log. */
|
/** Renders a {@link QuestView} as styled text for the console quest log. */
|
||||||
public final class QuestText {
|
public final class QuestText {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private QuestText() {
|
private QuestText() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the quest log: active quests with their objective, then the
|
||||||
|
* completed titles.
|
||||||
|
*
|
||||||
|
* @param view the quest snapshot to render
|
||||||
|
* @return the rendered log, or a placeholder when there are no quests
|
||||||
|
*/
|
||||||
public static StyledText render(QuestView view) {
|
public static StyledText render(QuestView view) {
|
||||||
if (view.active().isEmpty() && view.completed().isEmpty()) {
|
if (view.active().isEmpty() && view.completed().isEmpty()) {
|
||||||
return StyledText.of("You have no quests yet.");
|
return StyledText.of("You have no quests yet.");
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.io.text;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Frontend-agnostic snapshot of the quest log. */
|
/**
|
||||||
|
* Frontend-agnostic snapshot of the quest log.
|
||||||
|
*
|
||||||
|
* @param active quests in progress, with their current objective
|
||||||
|
* @param completed titles of the quests already finished
|
||||||
|
*/
|
||||||
public record QuestView(List<QuestEntry> active, List<String> completed) {
|
public record QuestView(List<QuestEntry> active, List<String> completed) {
|
||||||
|
/** Defensively copies both lists, making the snapshot immutable. */
|
||||||
public QuestView {
|
public QuestView {
|
||||||
active = List.copyOf(active);
|
active = List.copyOf(active);
|
||||||
completed = List.copyOf(completed);
|
completed = List.copyOf(completed);
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import java.util.List;
|
|||||||
/** Default, layout-free rendering of high-level views into {@link StyledText}. */
|
/** Default, layout-free rendering of high-level views into {@link StyledText}. */
|
||||||
public final class Renderings {
|
public final class Renderings {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private Renderings() {
|
private Renderings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generic room rendering used by non-overriding {@code GameIO}s (e.g. tests). */
|
/**
|
||||||
|
* Generic room rendering used by non-overriding {@code GameIO}s (e.g. tests).
|
||||||
|
*
|
||||||
|
* @param r the room snapshot to render
|
||||||
|
* @return name, description, items, NPCs and exits as styled text
|
||||||
|
*/
|
||||||
public static StyledText roomToStyledText(RoomView r) {
|
public static StyledText roomToStyledText(RoomView r) {
|
||||||
StyledText.Builder b = StyledText.builder();
|
StyledText.Builder b = StyledText.builder();
|
||||||
b.heading(r.name()).plain("\n");
|
b.heading(r.name()).plain("\n");
|
||||||
@@ -33,6 +39,13 @@ public final class Renderings {
|
|||||||
return b.build();
|
return b.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends the entries as a comma-separated list in the given style.
|
||||||
|
*
|
||||||
|
* @param b the builder to append to
|
||||||
|
* @param xs the entries
|
||||||
|
* @param st style applied to each entry
|
||||||
|
*/
|
||||||
private static void appendList(StyledText.Builder b, List<String> xs, Style st) {
|
private static void appendList(StyledText.Builder b, List<String> xs, Style st) {
|
||||||
for (int i = 0; i < xs.size(); i++) {
|
for (int i = 0; i < xs.size(); i++) {
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
package thb.jeanluc.adventure.io.text;
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
/** A room placed on the map grid at integer coords, with its fog-of-war state. */
|
/**
|
||||||
|
* A room placed on the map grid at integer coords, with its fog-of-war state.
|
||||||
|
*
|
||||||
|
* @param id unique slug of the room
|
||||||
|
* @param name display name shown inside the cell
|
||||||
|
* @param x column on the map grid, 0-based
|
||||||
|
* @param y row on the map grid, 0-based
|
||||||
|
* @param state how much the player knows about this room
|
||||||
|
*/
|
||||||
public record RoomCell(String id, String name, int x, int y, CellState state) {}
|
public record RoomCell(String id, String name, int x, int y, CellState state) {}
|
||||||
|
|||||||
@@ -2,9 +2,18 @@ package thb.jeanluc.adventure.io.text;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Semantic snapshot of a room for rendering. Display-ready strings, no layout. */
|
/**
|
||||||
|
* Semantic snapshot of a room for rendering. Display-ready strings, no layout.
|
||||||
|
*
|
||||||
|
* @param name display name of the room
|
||||||
|
* @param description room description text
|
||||||
|
* @param items display names of the items visible in the room
|
||||||
|
* @param npcs display names of the NPCs present in the room
|
||||||
|
* @param exits directions the player can leave by
|
||||||
|
*/
|
||||||
public record RoomView(String name, String description,
|
public record RoomView(String name, String description,
|
||||||
List<String> items, List<String> npcs, List<String> exits) {
|
List<String> items, List<String> npcs, List<String> exits) {
|
||||||
|
/** Defensively copies the lists, making the snapshot immutable. */
|
||||||
public RoomView {
|
public RoomView {
|
||||||
items = List.copyOf(items);
|
items = List.copyOf(items);
|
||||||
npcs = List.copyOf(npcs);
|
npcs = List.copyOf(npcs);
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
package thb.jeanluc.adventure.io.text;
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
/** A run of text carrying a single semantic {@link Style}. */
|
/**
|
||||||
|
* A run of text carrying a single semantic {@link Style}.
|
||||||
|
*
|
||||||
|
* @param text the text of this run; never null
|
||||||
|
* @param style the semantic role applied to the whole run; never null
|
||||||
|
*/
|
||||||
public record Span(String text, Style style) {
|
public record Span(String text, Style style) {
|
||||||
|
/**
|
||||||
|
* Rejects null components.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if {@code text} or {@code style} is null
|
||||||
|
*/
|
||||||
public Span {
|
public Span {
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
throw new IllegalArgumentException("text must not be null");
|
throw new IllegalArgumentException("text must not be null");
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
package thb.jeanluc.adventure.io.text;
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
/** Semantic output roles. Renderers map each role to concrete colours/attributes. */
|
/** Semantic output roles. Renderers map each role to concrete colours/attributes. */
|
||||||
public enum Style { PLAIN, HEADING, ITEM, NPC, EXIT, DANGER, DIM }
|
public enum Style {
|
||||||
|
|
||||||
|
/** Body text without emphasis. */
|
||||||
|
PLAIN,
|
||||||
|
|
||||||
|
/** Titles and section headers. */
|
||||||
|
HEADING,
|
||||||
|
|
||||||
|
/** Item names. */
|
||||||
|
ITEM,
|
||||||
|
|
||||||
|
/** NPC names. */
|
||||||
|
NPC,
|
||||||
|
|
||||||
|
/** Exit directions and room links. */
|
||||||
|
EXIT,
|
||||||
|
|
||||||
|
/** Warnings, failures and threats. */
|
||||||
|
DANGER,
|
||||||
|
|
||||||
|
/** Secondary text of lower importance, e.g. the HUD line or echoed input. */
|
||||||
|
DIM
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,18 +6,30 @@ import java.util.List;
|
|||||||
/** An ordered, immutable list of styled {@link Span}s. Built via {@link Builder}. */
|
/** An ordered, immutable list of styled {@link Span}s. Built via {@link Builder}. */
|
||||||
public final class StyledText {
|
public final class StyledText {
|
||||||
|
|
||||||
|
/** The spans of this text, in output order; unmodifiable. */
|
||||||
private final List<Span> spans;
|
private final List<Span> spans;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param spans the spans to copy into this text
|
||||||
|
*/
|
||||||
private StyledText(List<Span> spans) {
|
private StyledText(List<Span> spans) {
|
||||||
this.spans = List.copyOf(spans);
|
this.spans = List.copyOf(spans);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return the spans in order (unmodifiable). */
|
/**
|
||||||
|
* Exposes the spans for rendering.
|
||||||
|
*
|
||||||
|
* @return the spans in order (unmodifiable)
|
||||||
|
*/
|
||||||
public List<Span> spans() {
|
public List<Span> spans() {
|
||||||
return spans;
|
return spans;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return all span text concatenated, without any styling. */
|
/**
|
||||||
|
* Flattens this text for frontends that cannot render styles.
|
||||||
|
*
|
||||||
|
* @return all span text concatenated, without any styling
|
||||||
|
*/
|
||||||
public String plainText() {
|
public String plainText() {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (Span s : spans) {
|
for (Span s : spans) {
|
||||||
@@ -26,32 +38,104 @@ public final class StyledText {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return a styled text of a single {@link Style#PLAIN} span. */
|
/**
|
||||||
|
* Wraps an unstyled string.
|
||||||
|
*
|
||||||
|
* @param s the text
|
||||||
|
* @return a styled text of a single {@link Style#PLAIN} span
|
||||||
|
*/
|
||||||
public static StyledText of(String s) {
|
public static StyledText of(String s) {
|
||||||
return new StyledText(List.of(new Span(s, Style.PLAIN)));
|
return new StyledText(List.of(new Span(s, Style.PLAIN)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts assembling a styled text span by span.
|
||||||
|
*
|
||||||
|
* @return a new, empty builder
|
||||||
|
*/
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
return new Builder();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fluent builder; one method per {@link Style} role. */
|
/** Fluent builder; one method per {@link Style} role. */
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
|
|
||||||
|
/** Spans collected so far, in the order the append methods were called. */
|
||||||
private final List<Span> spans = new ArrayList<>();
|
private final List<Span> spans = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends one span.
|
||||||
|
*
|
||||||
|
* @param text the text to append
|
||||||
|
* @param style the role to apply to it
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
private Builder add(String text, Style style) {
|
private Builder add(String text, Style style) {
|
||||||
spans.add(new Span(text, style));
|
spans.add(new Span(text, style));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a span in the {@link Style#PLAIN} role.
|
||||||
|
*
|
||||||
|
* @param s text to append as {@link Style#PLAIN}
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder plain(String s) { return add(s, Style.PLAIN); }
|
public Builder plain(String s) { return add(s, Style.PLAIN); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a span in the {@link Style#HEADING} role.
|
||||||
|
*
|
||||||
|
* @param s text to append as {@link Style#HEADING}
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder heading(String s) { return add(s, Style.HEADING); }
|
public Builder heading(String s) { return add(s, Style.HEADING); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a span in the {@link Style#ITEM} role.
|
||||||
|
*
|
||||||
|
* @param s text to append as {@link Style#ITEM}
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder item(String s) { return add(s, Style.ITEM); }
|
public Builder item(String s) { return add(s, Style.ITEM); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a span in the {@link Style#NPC} role.
|
||||||
|
*
|
||||||
|
* @param s text to append as {@link Style#NPC}
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder npc(String s) { return add(s, Style.NPC); }
|
public Builder npc(String s) { return add(s, Style.NPC); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a span in the {@link Style#EXIT} role.
|
||||||
|
*
|
||||||
|
* @param s text to append as {@link Style#EXIT}
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder exit(String s) { return add(s, Style.EXIT); }
|
public Builder exit(String s) { return add(s, Style.EXIT); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a span in the {@link Style#DANGER} role.
|
||||||
|
*
|
||||||
|
* @param s text to append as {@link Style#DANGER}
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder danger(String s) { return add(s, Style.DANGER); }
|
public Builder danger(String s) { return add(s, Style.DANGER); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a span in the {@link Style#DIM} role.
|
||||||
|
*
|
||||||
|
* @param s text to append as {@link Style#DIM}
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder dim(String s) { return add(s, Style.DIM); }
|
public Builder dim(String s) { return add(s, Style.DIM); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finishes the build; the builder may not be reused meaningfully afterwards.
|
||||||
|
*
|
||||||
|
* @return an immutable styled text of the collected spans
|
||||||
|
*/
|
||||||
public StyledText build() {
|
public StyledText build() {
|
||||||
return new StyledText(spans);
|
return new StyledText(spans);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,19 @@ import java.util.Map;
|
|||||||
/** Builds {@link Combination}s from {@link CombinationDto}s, validating item ids. */
|
/** Builds {@link Combination}s from {@link CombinationDto}s, validating item ids. */
|
||||||
public final class CombinationFactory {
|
public final class CombinationFactory {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private CombinationFactory() {
|
private CombinationFactory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a combination and checks every referenced item id against the registry.
|
||||||
|
*
|
||||||
|
* @param dto data read from {@code combinations.yaml}; must not be null
|
||||||
|
* @param items the global item registry
|
||||||
|
* @return the freshly built combination
|
||||||
|
* @throws WorldLoadException if an item id is missing, unknown, or both
|
||||||
|
* ingredients are the same item
|
||||||
|
*/
|
||||||
public static Combination fromDto(CombinationDto dto, Map<String, Item> items) {
|
public static Combination fromDto(CombinationDto dto, Map<String, Item> items) {
|
||||||
requireItem(items, dto.a(), "a");
|
requireItem(items, dto.a(), "a");
|
||||||
requireItem(items, dto.b(), "b");
|
requireItem(items, dto.b(), "b");
|
||||||
@@ -41,6 +51,14 @@ public final class CombinationFactory {
|
|||||||
dto.failText());
|
dto.failText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that {@code id} is present and known to the item registry.
|
||||||
|
*
|
||||||
|
* @param items the global item registry
|
||||||
|
* @param id the item id to check
|
||||||
|
* @param field name of the DTO field, used in the error message
|
||||||
|
* @throws WorldLoadException if the id is null or unknown
|
||||||
|
*/
|
||||||
private static void requireItem(Map<String, Item> items, String id, String field) {
|
private static void requireItem(Map<String, Item> items, String id, String field) {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
throw new WorldLoadException("Combination '" + field + "' is missing an item id");
|
throw new WorldLoadException("Combination '" + field + "' is missing an item id");
|
||||||
|
|||||||
@@ -7,9 +7,17 @@ import thb.jeanluc.adventure.model.Ending;
|
|||||||
/** Builds {@link Ending} objects from {@link EndingDto}s. */
|
/** Builds {@link Ending} objects from {@link EndingDto}s. */
|
||||||
public final class EndingFactory {
|
public final class EndingFactory {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private EndingFactory() {
|
private EndingFactory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an ending, defaulting a null {@code victory} flag to false.
|
||||||
|
*
|
||||||
|
* @param dto data read from {@code endings.yaml}; must not be null
|
||||||
|
* @return the freshly built ending
|
||||||
|
* @throws WorldLoadException if a condition sets none of its fields
|
||||||
|
*/
|
||||||
public static Ending fromDto(EndingDto dto) {
|
public static Ending fromDto(EndingDto dto) {
|
||||||
return new Ending(
|
return new Ending(
|
||||||
dto.id(),
|
dto.id(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import thb.jeanluc.adventure.model.Npc;
|
|||||||
*/
|
*/
|
||||||
public final class NpcFactory {
|
public final class NpcFactory {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private NpcFactory() {
|
private NpcFactory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,17 @@ import java.util.List;
|
|||||||
/** Builds {@link Quest} objects from {@link QuestDto}s (no reference resolution needed). */
|
/** Builds {@link Quest} objects from {@link QuestDto}s (no reference resolution needed). */
|
||||||
public final class QuestFactory {
|
public final class QuestFactory {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private QuestFactory() {
|
private QuestFactory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a quest including all of its stages.
|
||||||
|
*
|
||||||
|
* @param dto data read from {@code quests.yaml}; must not be null
|
||||||
|
* @return the freshly built quest
|
||||||
|
* @throws WorldLoadException if a condition or effect sets none of its fields
|
||||||
|
*/
|
||||||
public static Quest fromDto(QuestDto dto) {
|
public static Quest fromDto(QuestDto dto) {
|
||||||
List<QuestStage> stages = new ArrayList<>();
|
List<QuestStage> stages = new ArrayList<>();
|
||||||
if (dto.stages() != null) {
|
if (dto.stages() != null) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import thb.jeanluc.adventure.model.Room;
|
|||||||
*/
|
*/
|
||||||
public final class RoomFactory {
|
public final class RoomFactory {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private RoomFactory() {
|
private RoomFactory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,18 +22,33 @@ public class TutorialLoader {
|
|||||||
/** Default classpath base directory. */
|
/** Default classpath base directory. */
|
||||||
public static final String DEFAULT_BASE = "/world";
|
public static final String DEFAULT_BASE = "/world";
|
||||||
|
|
||||||
|
/** Single Jackson mapper, configured once for YAML input. */
|
||||||
private final ObjectMapper yaml = new ObjectMapper(new YAMLFactory());
|
private final ObjectMapper yaml = new ObjectMapper(new YAMLFactory());
|
||||||
|
|
||||||
|
/** Classpath base directory (typically {@link #DEFAULT_BASE} or a test override). */
|
||||||
private final String basePath;
|
private final String basePath;
|
||||||
|
|
||||||
|
/** Convenience constructor using {@link #DEFAULT_BASE}. */
|
||||||
public TutorialLoader() {
|
public TutorialLoader() {
|
||||||
this(DEFAULT_BASE);
|
this(DEFAULT_BASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a loader reading from the given classpath base directory.
|
||||||
|
*
|
||||||
|
* @param basePath classpath base directory holding {@code tutorial.yaml}
|
||||||
|
*/
|
||||||
public TutorialLoader(String basePath) {
|
public TutorialLoader(String basePath) {
|
||||||
this.basePath = basePath;
|
this.basePath = basePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reads the tutorial; absent file → {@link Tutorial#none()}. */
|
/**
|
||||||
|
* Reads the tutorial; absent file → {@link Tutorial#none()}.
|
||||||
|
*
|
||||||
|
* @return the parsed tutorial, or {@link Tutorial#none()} if the file is
|
||||||
|
* missing or empty
|
||||||
|
* @throws WorldLoadException if the file exists but cannot be parsed
|
||||||
|
*/
|
||||||
public Tutorial load() {
|
public Tutorial load() {
|
||||||
String resource = basePath + "/tutorial.yaml";
|
String resource = basePath + "/tutorial.yaml";
|
||||||
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
||||||
|
|||||||
@@ -2,7 +2,18 @@ package thb.jeanluc.adventure.loader.dto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML representation of an item-combination recipe. */
|
/**
|
||||||
|
* YAML representation of an item-combination recipe.
|
||||||
|
*
|
||||||
|
* @param a id of the first ingredient item
|
||||||
|
* @param b id of the second ingredient item
|
||||||
|
* @param requires conditions that must hold for the recipe to apply; nullable
|
||||||
|
* @param consume ids of the items removed from the inventory on success; nullable
|
||||||
|
* @param produce id of the item handed to the player on success; nullable
|
||||||
|
* @param effects effects applied on success; nullable
|
||||||
|
* @param response text printed on success
|
||||||
|
* @param failText text printed when the required conditions are not met; nullable
|
||||||
|
*/
|
||||||
public record CombinationDto(
|
public record CombinationDto(
|
||||||
String a,
|
String a,
|
||||||
String b,
|
String b,
|
||||||
|
|||||||
@@ -6,9 +6,21 @@ import thb.jeanluc.adventure.model.Condition;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML condition: exactly one of the fields is set. */
|
/**
|
||||||
|
* YAML condition: exactly one of the fields is set.
|
||||||
|
*
|
||||||
|
* @param flag name of a flag that must be set; nullable
|
||||||
|
* @param notFlag name of a flag that must not be set; nullable
|
||||||
|
* @param hasItem id of an item the player must carry; nullable
|
||||||
|
*/
|
||||||
public record ConditionDto(String flag, String notFlag, String hasItem) {
|
public record ConditionDto(String flag, String notFlag, String hasItem) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this DTO into its domain condition.
|
||||||
|
*
|
||||||
|
* @return the matching {@link Condition}
|
||||||
|
* @throws WorldLoadException if none of the fields is set
|
||||||
|
*/
|
||||||
public Condition toModel() {
|
public Condition toModel() {
|
||||||
if (flag != null) {
|
if (flag != null) {
|
||||||
return new Condition(Condition.Type.FLAG, flag);
|
return new Condition(Condition.Type.FLAG, flag);
|
||||||
@@ -22,6 +34,13 @@ public record ConditionDto(String flag, String notFlag, String hasItem) {
|
|||||||
throw new WorldLoadException("Condition must set one of flag/notFlag/hasItem");
|
throw new WorldLoadException("Condition must set one of flag/notFlag/hasItem");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list of DTOs into domain conditions.
|
||||||
|
*
|
||||||
|
* @param dtos the DTOs to convert; null is treated as empty
|
||||||
|
* @return the converted conditions, never null
|
||||||
|
* @throws WorldLoadException if any DTO sets none of its fields
|
||||||
|
*/
|
||||||
public static List<Condition> toModelList(List<ConditionDto> dtos) {
|
public static List<Condition> toModelList(List<ConditionDto> dtos) {
|
||||||
List<Condition> out = new ArrayList<>();
|
List<Condition> out = new ArrayList<>();
|
||||||
if (dtos != null) {
|
if (dtos != null) {
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package thb.jeanluc.adventure.loader.dto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML representation of a conditional room description. */
|
/**
|
||||||
|
* YAML representation of a conditional room description.
|
||||||
|
*
|
||||||
|
* @param when conditions that must all hold for this variant to be used
|
||||||
|
* @param text description text used instead of the room's default
|
||||||
|
*/
|
||||||
public record DescriptionStateDto(List<ConditionDto> when, String text) {
|
public record DescriptionStateDto(List<ConditionDto> when, String text) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package thb.jeanluc.adventure.loader.dto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML representation of a conditional NPC line. */
|
/**
|
||||||
|
* YAML representation of a conditional NPC line.
|
||||||
|
*
|
||||||
|
* @param when conditions that must all hold for this line to be spoken
|
||||||
|
* @param text text the NPC says
|
||||||
|
*/
|
||||||
public record DialogueLineDto(List<ConditionDto> when, String text) {
|
public record DialogueLineDto(List<ConditionDto> when, String text) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,38 @@ import thb.jeanluc.adventure.model.Effect;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML effect: exactly one of the fields is set. */
|
/**
|
||||||
|
* YAML effect: exactly one of the fields is set.
|
||||||
|
*
|
||||||
|
* @param setFlag name of a flag to set; nullable
|
||||||
|
* @param clearFlag name of a flag to clear; nullable
|
||||||
|
* @param giveItem id of an item to add to the inventory; nullable
|
||||||
|
* @param removeItem id of an item to take from the inventory; nullable
|
||||||
|
* @param say text to print; nullable
|
||||||
|
* @param startQuest id of a quest to start; nullable
|
||||||
|
*/
|
||||||
public record EffectDto(String setFlag, String clearFlag, String giveItem, String removeItem,
|
public record EffectDto(String setFlag, String clearFlag, String giveItem, String removeItem,
|
||||||
String say, String startQuest) {
|
String say, String startQuest) {
|
||||||
|
|
||||||
/** Backward-compatible constructor without startQuest. */
|
/**
|
||||||
|
* Backward-compatible constructor without startQuest.
|
||||||
|
*
|
||||||
|
* @param setFlag name of a flag to set; nullable
|
||||||
|
* @param clearFlag name of a flag to clear; nullable
|
||||||
|
* @param giveItem id of an item to add to the inventory; nullable
|
||||||
|
* @param removeItem id of an item to take from the inventory; nullable
|
||||||
|
* @param say text to print; nullable
|
||||||
|
*/
|
||||||
public EffectDto(String setFlag, String clearFlag, String giveItem, String removeItem, String say) {
|
public EffectDto(String setFlag, String clearFlag, String giveItem, String removeItem, String say) {
|
||||||
this(setFlag, clearFlag, giveItem, removeItem, say, null);
|
this(setFlag, clearFlag, giveItem, removeItem, say, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this DTO into its domain effect.
|
||||||
|
*
|
||||||
|
* @return the matching {@link Effect}
|
||||||
|
* @throws WorldLoadException if none of the fields is set
|
||||||
|
*/
|
||||||
public Effect toModel() {
|
public Effect toModel() {
|
||||||
if (setFlag != null) {
|
if (setFlag != null) {
|
||||||
return new Effect(Effect.Type.SET_FLAG, setFlag);
|
return new Effect(Effect.Type.SET_FLAG, setFlag);
|
||||||
@@ -38,6 +61,13 @@ public record EffectDto(String setFlag, String clearFlag, String giveItem, Strin
|
|||||||
"Effect must set one of setFlag/clearFlag/giveItem/removeItem/say/startQuest");
|
"Effect must set one of setFlag/clearFlag/giveItem/removeItem/say/startQuest");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list of DTOs into domain effects.
|
||||||
|
*
|
||||||
|
* @param dtos the DTOs to convert; null is treated as empty
|
||||||
|
* @return the converted effects, never null
|
||||||
|
* @throws WorldLoadException if any DTO sets none of its fields
|
||||||
|
*/
|
||||||
public static List<Effect> toModelList(List<EffectDto> dtos) {
|
public static List<Effect> toModelList(List<EffectDto> dtos) {
|
||||||
List<Effect> out = new ArrayList<>();
|
List<Effect> out = new ArrayList<>();
|
||||||
if (dtos != null) {
|
if (dtos != null) {
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ package thb.jeanluc.adventure.loader.dto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML representation of a game ending. */
|
/**
|
||||||
|
* YAML representation of a game ending.
|
||||||
|
*
|
||||||
|
* @param id unique slug
|
||||||
|
* @param title display title of the ending
|
||||||
|
* @param victory whether this ending counts as a win; defaults to false if null
|
||||||
|
* @param when conditions that must all hold for this ending to trigger
|
||||||
|
* @param text text printed when the ending triggers
|
||||||
|
*/
|
||||||
public record EndingDto(String id, String title, Boolean victory, List<ConditionDto> when, String text) {
|
public record EndingDto(String id, String title, Boolean victory, List<ConditionDto> when, String text) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ package thb.jeanluc.adventure.loader.dto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML representation of a single condition-gated exit. */
|
/**
|
||||||
|
* YAML representation of a single condition-gated exit.
|
||||||
|
*
|
||||||
|
* @param direction name of the gated exit direction
|
||||||
|
* @param requires conditions that must all hold for the exit to be passable
|
||||||
|
* @param blocked text printed when the exit is used while still locked
|
||||||
|
*/
|
||||||
public record ExitLockDto(String direction, List<ConditionDto> requires, String blocked) {
|
public record ExitLockDto(String direction, List<ConditionDto> requires, String blocked) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,15 @@ public record NpcDto(
|
|||||||
List<ReactionDto> reactions,
|
List<ReactionDto> reactions,
|
||||||
List<DialogueLineDto> dialogue
|
List<DialogueLineDto> dialogue
|
||||||
) {
|
) {
|
||||||
/** Backward-compatible constructor without dialogue. */
|
/**
|
||||||
|
* Backward-compatible constructor without dialogue, which defaults to null.
|
||||||
|
*
|
||||||
|
* @param id unique slug
|
||||||
|
* @param name display name
|
||||||
|
* @param description examine description
|
||||||
|
* @param greeting text the NPC says on {@code talk}
|
||||||
|
* @param reactions list of trigger/response definitions; may be null
|
||||||
|
*/
|
||||||
public NpcDto(String id, String name, String description, String greeting, List<ReactionDto> reactions) {
|
public NpcDto(String id, String name, String description, String greeting, List<ReactionDto> reactions) {
|
||||||
this(id, name, description, greeting, reactions, null);
|
this(id, name, description, greeting, reactions, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ package thb.jeanluc.adventure.loader.dto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML representation of a quest. */
|
/**
|
||||||
|
* YAML representation of a quest.
|
||||||
|
*
|
||||||
|
* @param id unique slug
|
||||||
|
* @param title display title
|
||||||
|
* @param autoStart whether the quest starts with the game; defaults to false if null
|
||||||
|
* @param stages ordered stages the player works through
|
||||||
|
* @param onComplete effects applied once the last stage is done; nullable
|
||||||
|
*/
|
||||||
public record QuestDto(String id, String title, Boolean autoStart,
|
public record QuestDto(String id, String title, Boolean autoStart,
|
||||||
List<QuestStageDto> stages, List<EffectDto> onComplete) {
|
List<QuestStageDto> stages, List<EffectDto> onComplete) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ package thb.jeanluc.adventure.loader.dto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML representation of a quest stage. */
|
/**
|
||||||
|
* YAML representation of a quest stage.
|
||||||
|
*
|
||||||
|
* @param objective objective text shown in the quest log
|
||||||
|
* @param completion conditions that must all hold for the stage to be done
|
||||||
|
* @param onComplete effects applied when the stage completes; nullable
|
||||||
|
*/
|
||||||
public record QuestStageDto(String objective, List<ConditionDto> completion, List<EffectDto> onComplete) {
|
public record QuestStageDto(String objective, List<ConditionDto> completion, List<EffectDto> onComplete) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ public record ReactionDto(
|
|||||||
List<ConditionDto> requires,
|
List<ConditionDto> requires,
|
||||||
List<EffectDto> effects
|
List<EffectDto> effects
|
||||||
) {
|
) {
|
||||||
/** Backward-compatible constructor without requires/effects. */
|
/**
|
||||||
|
* Backward-compatible constructor without requires/effects, which default to null.
|
||||||
|
*
|
||||||
|
* @param onReceive id of the item that triggers this reaction
|
||||||
|
* @param response text the NPC says after the exchange
|
||||||
|
* @param gives id of the item handed back; nullable
|
||||||
|
* @param consumes id of the item taken from the player; nullable
|
||||||
|
*/
|
||||||
public ReactionDto(String onReceive, String response, String gives, String consumes) {
|
public ReactionDto(String onReceive, String response, String gives, String consumes) {
|
||||||
this(onReceive, response, gives, consumes, null, null);
|
this(onReceive, response, gives, consumes, null, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,16 @@ public record RoomDto(
|
|||||||
Boolean dark,
|
Boolean dark,
|
||||||
String music
|
String music
|
||||||
) {
|
) {
|
||||||
/** Backward-compatible constructor without the optional state fields. */
|
/**
|
||||||
|
* Backward-compatible constructor without the optional state fields, which default to null.
|
||||||
|
*
|
||||||
|
* @param id unique slug
|
||||||
|
* @param name display name
|
||||||
|
* @param description description shown by {@code look}
|
||||||
|
* @param exits direction-name to target-room-id map
|
||||||
|
* @param items ids of items initially in this room
|
||||||
|
* @param npcs ids of NPCs initially in this room
|
||||||
|
*/
|
||||||
public RoomDto(String id, String name, String description,
|
public RoomDto(String id, String name, String description,
|
||||||
Map<String, String> exits, List<String> items, List<String> npcs) {
|
Map<String, String> exits, List<String> items, List<String> npcs) {
|
||||||
this(id, name, description, exits, items, npcs, null, null, null, null);
|
this(id, name, description, exits, items, npcs, null, null, null, null);
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ package thb.jeanluc.adventure.loader.dto;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** YAML representation of the tutorial document. */
|
/**
|
||||||
|
* YAML representation of the tutorial document.
|
||||||
|
*
|
||||||
|
* @param intro text shown before the first step
|
||||||
|
* @param steps ordered tutorial steps
|
||||||
|
* @param closingTips text shown after the last step
|
||||||
|
*/
|
||||||
public record TutorialDto(String intro, List<TutorialStepDto> steps, String closingTips) {
|
public record TutorialDto(String intro, List<TutorialStepDto> steps, String closingTips) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
package thb.jeanluc.adventure.loader.dto;
|
package thb.jeanluc.adventure.loader.dto;
|
||||||
|
|
||||||
/** YAML representation of a tutorial step. */
|
/**
|
||||||
|
* YAML representation of a tutorial step.
|
||||||
|
*
|
||||||
|
* @param instruction text telling the player what to type
|
||||||
|
* @param expect command verb the step accepts
|
||||||
|
* @param minArgs minimum number of arguments the command must carry; 0 if null
|
||||||
|
* @param confirm text printed once the step is passed
|
||||||
|
* @param hint text printed when the player types something else
|
||||||
|
*/
|
||||||
public record TutorialStepDto(
|
public record TutorialStepDto(
|
||||||
String instruction,
|
String instruction,
|
||||||
String expect,
|
String expect,
|
||||||
|
|||||||
@@ -10,17 +10,29 @@ import java.util.List;
|
|||||||
/** Frontend-agnostic main menu, slot picker, and new-game name prompt. */
|
/** Frontend-agnostic main menu, slot picker, and new-game name prompt. */
|
||||||
public final class MainMenu {
|
public final class MainMenu {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private MainMenu() {
|
private MainMenu() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shows the main menu and returns the chosen action. */
|
/**
|
||||||
|
* Shows the main menu and returns the chosen action.
|
||||||
|
*
|
||||||
|
* @param io frontend to render the menu on
|
||||||
|
* @return the action the player picked
|
||||||
|
*/
|
||||||
public static MenuAction show(GameIO io) {
|
public static MenuAction show(GameIO io) {
|
||||||
int i = io.choose("HAUNTED MANOR",
|
int i = io.choose("HAUNTED MANOR",
|
||||||
List.of("New Game", "Load Game", "Settings", "Quit"));
|
List.of("New Game", "Load Game", "Settings", "Quit"));
|
||||||
return MenuAction.values()[i];
|
return MenuAction.values()[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prompts for a new save name; blank input yields {@code defaultName}. */
|
/**
|
||||||
|
* Prompts for a new save name; blank input yields {@code defaultName}.
|
||||||
|
*
|
||||||
|
* @param io frontend to prompt on
|
||||||
|
* @param defaultName name used when the player enters nothing
|
||||||
|
* @return the trimmed save name
|
||||||
|
*/
|
||||||
public static String promptName(GameIO io, String defaultName) {
|
public static String promptName(GameIO io, String defaultName) {
|
||||||
io.write("Name your save (Enter for \"" + defaultName + "\"):");
|
io.write("Name your save (Enter for \"" + defaultName + "\"):");
|
||||||
String line = io.readLine();
|
String line = io.readLine();
|
||||||
@@ -31,6 +43,10 @@ public final class MainMenu {
|
|||||||
* Lets the player pick a save slot to load. Returns the chosen
|
* Lets the player pick a save slot to load. Returns the chosen
|
||||||
* {@link SaveSlotInfo}, or {@code null} if there are none or the player
|
* {@link SaveSlotInfo}, or {@code null} if there are none or the player
|
||||||
* picks "Back".
|
* picks "Back".
|
||||||
|
*
|
||||||
|
* @param io frontend to render the slot list on
|
||||||
|
* @param saves source of the available slots
|
||||||
|
* @return the selected slot, or {@code null} if nothing was selected
|
||||||
*/
|
*/
|
||||||
public static SaveSlotInfo pickSlot(GameIO io, SaveService saves) {
|
public static SaveSlotInfo pickSlot(GameIO io, SaveService saves) {
|
||||||
List<SaveSlotInfo> slots = saves.list();
|
List<SaveSlotInfo> slots = saves.list();
|
||||||
|
|||||||
@@ -2,5 +2,16 @@ package thb.jeanluc.adventure.menu;
|
|||||||
|
|
||||||
/** Top-level main-menu choices, in display order. */
|
/** Top-level main-menu choices, in display order. */
|
||||||
public enum MenuAction {
|
public enum MenuAction {
|
||||||
NEW_GAME, LOAD, SETTINGS, QUIT
|
|
||||||
|
/** Start a new game in a freshly named save slot. */
|
||||||
|
NEW_GAME,
|
||||||
|
|
||||||
|
/** Continue from an existing save slot. */
|
||||||
|
LOAD,
|
||||||
|
|
||||||
|
/** Open the settings screen. */
|
||||||
|
SETTINGS,
|
||||||
|
|
||||||
|
/** Leave the application. */
|
||||||
|
QUIT
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,18 @@ import java.util.List;
|
|||||||
/** Settings screen: cycle colour, glyph, and music level; persists + applies. */
|
/** Settings screen: cycle colour, glyph, and music level; persists + applies. */
|
||||||
public final class SettingsMenu {
|
public final class SettingsMenu {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private SettingsMenu() {
|
private SettingsMenu() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loops the settings screen until "Back", persisting + applying each change. */
|
/**
|
||||||
|
* Loops the settings screen until "Back", persisting + applying each change.
|
||||||
|
*
|
||||||
|
* @param io frontend to render the screen on
|
||||||
|
* @param current settings the screen starts from
|
||||||
|
* @param store destination the changed settings are written to
|
||||||
|
* @return the settings in effect when the player left the screen
|
||||||
|
*/
|
||||||
public static Settings show(GameIO io, Settings current, SettingsStore store) {
|
public static Settings show(GameIO io, Settings current, SettingsStore store) {
|
||||||
Settings s = current;
|
Settings s = current;
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -36,7 +44,12 @@ public final class SettingsMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies settings to the IO: music level always; colour/glyph only on the console. */
|
/**
|
||||||
|
* Applies settings to the IO: music level always; colour/glyph only on the console.
|
||||||
|
*
|
||||||
|
* @param s settings to apply
|
||||||
|
* @param io frontend the settings are applied to
|
||||||
|
*/
|
||||||
public static void apply(Settings s, GameIO io) {
|
public static void apply(Settings s, GameIO io) {
|
||||||
io.setMusicLevel(s.musicLevel());
|
io.setMusicLevel(s.musicLevel());
|
||||||
if (io instanceof ConsoleIO c) {
|
if (io instanceof ConsoleIO c) {
|
||||||
@@ -45,6 +58,7 @@ public final class SettingsMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Next colour mode in the cycle AUTO {@literal ->} ON {@literal ->} OFF. */
|
||||||
private static ConsoleIO.ColorMode nextColor(ConsoleIO.ColorMode m) {
|
private static ConsoleIO.ColorMode nextColor(ConsoleIO.ColorMode m) {
|
||||||
return switch (m) {
|
return switch (m) {
|
||||||
case AUTO -> ConsoleIO.ColorMode.ON;
|
case AUTO -> ConsoleIO.ColorMode.ON;
|
||||||
@@ -53,6 +67,7 @@ public final class SettingsMenu {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Next glyph mode in the cycle UNICODE {@literal ->} ASCII {@literal ->} GLYPH. */
|
||||||
private static ConsoleIO.GlyphMode nextGlyph(ConsoleIO.GlyphMode m) {
|
private static ConsoleIO.GlyphMode nextGlyph(ConsoleIO.GlyphMode m) {
|
||||||
return switch (m) {
|
return switch (m) {
|
||||||
case UNICODE -> ConsoleIO.GlyphMode.ASCII;
|
case UNICODE -> ConsoleIO.GlyphMode.ASCII;
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import java.util.List;
|
|||||||
* A data-driven item-combination recipe. Identified by an unordered pair of
|
* A data-driven item-combination recipe. Identified by an unordered pair of
|
||||||
* item ids ({@code a}, {@code b}). When applied it may require conditions,
|
* item ids ({@code a}, {@code b}). When applied it may require conditions,
|
||||||
* consume items, produce an item, apply effects, and print a response.
|
* consume items, produce an item, apply effects, and print a response.
|
||||||
|
*
|
||||||
|
* @param a id of the first item of the pair
|
||||||
|
* @param b id of the second item of the pair
|
||||||
|
* @param requires conditions that must hold for the recipe to apply; empty = always
|
||||||
|
* @param consume ids of the items removed from the player's inventory on success
|
||||||
|
* @param produce id of the item handed to the player on success; nullable
|
||||||
|
* @param effects effects applied on success
|
||||||
|
* @param response text written on success
|
||||||
|
* @param failText text written when {@code requires} does not hold; nullable
|
||||||
*/
|
*/
|
||||||
public record Combination(
|
public record Combination(
|
||||||
String a,
|
String a,
|
||||||
@@ -17,18 +26,29 @@ public record Combination(
|
|||||||
String response,
|
String response,
|
||||||
String failText
|
String failText
|
||||||
) {
|
) {
|
||||||
|
/** Normalises the list components to immutable, non-null lists. */
|
||||||
public Combination {
|
public Combination {
|
||||||
requires = requires == null ? List.of() : List.copyOf(requires);
|
requires = requires == null ? List.of() : List.copyOf(requires);
|
||||||
consume = consume == null ? List.of() : List.copyOf(consume);
|
consume = consume == null ? List.of() : List.copyOf(consume);
|
||||||
effects = effects == null ? List.of() : List.copyOf(effects);
|
effects = effects == null ? List.of() : List.copyOf(effects);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Order-independent key for an item pair (sorted, '|'-joined). */
|
/**
|
||||||
|
* Order-independent key for an item pair (sorted, '|'-joined).
|
||||||
|
*
|
||||||
|
* @param x id of one item
|
||||||
|
* @param y id of the other item
|
||||||
|
* @return the canonical key identifying the pair regardless of argument order
|
||||||
|
*/
|
||||||
public static String key(String x, String y) {
|
public static String key(String x, String y) {
|
||||||
return x.compareTo(y) <= 0 ? x + "|" + y : y + "|" + x;
|
return x.compareTo(y) <= 0 ? x + "|" + y : y + "|" + x;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This recipe's canonical pair key. */
|
/**
|
||||||
|
* This recipe's canonical pair key.
|
||||||
|
*
|
||||||
|
* @return the key under which the recipe is registered in the world
|
||||||
|
*/
|
||||||
public String key() {
|
public String key() {
|
||||||
return key(a, b);
|
return key(a, b);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
package thb.jeanluc.adventure.model;
|
package thb.jeanluc.adventure.model;
|
||||||
|
|
||||||
/** A single boolean test evaluated against world state and the player. */
|
/**
|
||||||
|
* A single boolean test evaluated against world state and the player.
|
||||||
|
*
|
||||||
|
* @param type the kind of test to perform
|
||||||
|
* @param arg the operand the test applies to: a flag name for {@link Type#FLAG}
|
||||||
|
* and {@link Type#NOT_FLAG}, an item id for {@link Type#HAS_ITEM}
|
||||||
|
*/
|
||||||
public record Condition(Type type, String arg) {
|
public record Condition(Type type, String arg) {
|
||||||
public enum Type { FLAG, NOT_FLAG, HAS_ITEM }
|
|
||||||
|
/** The kinds of test a {@link Condition} can perform. */
|
||||||
|
public enum Type {
|
||||||
|
/** Holds while the named flag is set in the world state. */
|
||||||
|
FLAG,
|
||||||
|
/** Holds while the named flag is not set in the world state. */
|
||||||
|
NOT_FLAG,
|
||||||
|
/** Holds while the player carries the named item. */
|
||||||
|
HAS_ITEM
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** An alternate room description shown while its conditions hold. */
|
/**
|
||||||
|
* An alternate room description shown while its conditions hold.
|
||||||
|
*
|
||||||
|
* @param when conditions that must all hold for this variant to be selected; empty = always
|
||||||
|
* @param text description text shown instead of the room's default description
|
||||||
|
*/
|
||||||
public record DescriptionState(List<Condition> when, String text) {
|
public record DescriptionState(List<Condition> when, String text) {
|
||||||
|
/** Normalises {@code when} to an immutable, non-null list. */
|
||||||
public DescriptionState {
|
public DescriptionState {
|
||||||
when = when == null ? List.of() : List.copyOf(when);
|
when = when == null ? List.of() : List.copyOf(when);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** A conditional line an NPC says on {@code talk}, first match wins. */
|
/**
|
||||||
|
* A conditional line an NPC says on {@code talk}, first match wins.
|
||||||
|
*
|
||||||
|
* @param when conditions that must all hold for this line to be selected; empty = always
|
||||||
|
* @param text text the NPC says when this line is selected
|
||||||
|
*/
|
||||||
public record DialogueLine(List<Condition> when, String text) {
|
public record DialogueLine(List<Condition> when, String text) {
|
||||||
|
/** Normalises {@code when} to an immutable, non-null list. */
|
||||||
public DialogueLine {
|
public DialogueLine {
|
||||||
when = when == null ? List.of() : List.copyOf(when);
|
when = when == null ? List.of() : List.copyOf(when);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,29 @@
|
|||||||
package thb.jeanluc.adventure.model;
|
package thb.jeanluc.adventure.model;
|
||||||
|
|
||||||
/** A single state mutation or message. */
|
/**
|
||||||
|
* A single state mutation or message.
|
||||||
|
*
|
||||||
|
* @param type the kind of mutation to apply
|
||||||
|
* @param arg the operand the mutation applies to: a flag name for
|
||||||
|
* {@link Type#SET_FLAG} and {@link Type#CLEAR_FLAG}, an item id for
|
||||||
|
* {@link Type#GIVE_ITEM} and {@link Type#REMOVE_ITEM}, the literal text
|
||||||
|
* for {@link Type#SAY}, a quest id for {@link Type#START_QUEST}
|
||||||
|
*/
|
||||||
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, START_QUEST }
|
|
||||||
|
/** The kinds of mutation an {@link Effect} can apply. */
|
||||||
|
public enum Type {
|
||||||
|
/** Sets the named flag in the world state. */
|
||||||
|
SET_FLAG,
|
||||||
|
/** Clears the named flag from the world state. */
|
||||||
|
CLEAR_FLAG,
|
||||||
|
/** Adds the named item to the player's inventory. */
|
||||||
|
GIVE_ITEM,
|
||||||
|
/** Takes the named item out of the player's inventory. */
|
||||||
|
REMOVE_ITEM,
|
||||||
|
/** Writes the argument to the player's output. */
|
||||||
|
SAY,
|
||||||
|
/** Activates the named quest. */
|
||||||
|
START_QUEST
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ package thb.jeanluc.adventure.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** A game ending: shown (and ends the game) when its conditions hold. */
|
/**
|
||||||
|
* A game ending: shown (and ends the game) when its conditions hold.
|
||||||
|
*
|
||||||
|
* @param id unique identifier of this ending
|
||||||
|
* @param title headline shown above the ending text
|
||||||
|
* @param victory whether reaching this ending counts as winning
|
||||||
|
* @param when conditions that must all hold for this ending to trigger
|
||||||
|
* @param text closing text shown to the player
|
||||||
|
*/
|
||||||
public record Ending(String id, String title, boolean victory, List<Condition> when, String text) {
|
public record Ending(String id, String title, boolean victory, List<Condition> when, String text) {
|
||||||
|
/** Normalises {@code when} to an immutable, non-null list. */
|
||||||
public Ending {
|
public Ending {
|
||||||
when = when == null ? List.of() : List.copyOf(when);
|
when = when == null ? List.of() : List.copyOf(when);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** A condition-gated exit with a message shown when it is blocked. */
|
/**
|
||||||
|
* A condition-gated exit with a message shown when it is blocked.
|
||||||
|
*
|
||||||
|
* @param requires conditions that must all hold for the exit to be passable
|
||||||
|
* @param blocked text written when the player tries the exit while it is blocked
|
||||||
|
*/
|
||||||
public record ExitLock(List<Condition> requires, String blocked) {
|
public record ExitLock(List<Condition> requires, String blocked) {
|
||||||
|
/** Normalises {@code requires} to an immutable, non-null list. */
|
||||||
public ExitLock {
|
public ExitLock {
|
||||||
requires = requires == null ? List.of() : List.copyOf(requires);
|
requires = requires == null ? List.of() : List.copyOf(requires);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,19 @@ package thb.jeanluc.adventure.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** A multi-stage quest. Stage completion is condition-driven; rewards are effects. */
|
/**
|
||||||
|
* A multi-stage quest. Stage completion is condition-driven; rewards are effects.
|
||||||
|
*
|
||||||
|
* @param id unique identifier of this quest, referenced by
|
||||||
|
* {@link Effect.Type#START_QUEST}
|
||||||
|
* @param title display name shown in the quest log
|
||||||
|
* @param autoStart whether the quest is active from the start of the game
|
||||||
|
* @param stages the stages in the order they must be completed
|
||||||
|
* @param onComplete effects applied once the final stage is completed
|
||||||
|
*/
|
||||||
public record Quest(String id, String title, boolean autoStart,
|
public record Quest(String id, String title, boolean autoStart,
|
||||||
List<QuestStage> stages, List<Effect> onComplete) {
|
List<QuestStage> stages, List<Effect> onComplete) {
|
||||||
|
/** Normalises the list components to immutable, non-null lists. */
|
||||||
public Quest {
|
public Quest {
|
||||||
stages = stages == null ? List.of() : List.copyOf(stages);
|
stages = stages == null ? List.of() : List.copyOf(stages);
|
||||||
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);
|
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ package thb.jeanluc.adventure.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** One stage of a {@link Quest}: an objective, a completion condition, optional rewards. */
|
/**
|
||||||
|
* One stage of a {@link Quest}: an objective, a completion condition, optional rewards.
|
||||||
|
*
|
||||||
|
* @param objective task description shown in the quest log while this stage is active
|
||||||
|
* @param completion conditions that must all hold for this stage to count as done
|
||||||
|
* @param onComplete effects applied when this stage is completed
|
||||||
|
*/
|
||||||
public record QuestStage(String objective, List<Condition> completion, List<Effect> onComplete) {
|
public record QuestStage(String objective, List<Condition> completion, List<Effect> onComplete) {
|
||||||
|
/** Normalises the list components to immutable, non-null lists. */
|
||||||
public QuestStage {
|
public QuestStage {
|
||||||
completion = completion == null ? List.of() : List.copyOf(completion);
|
completion = completion == null ? List.of() : List.copyOf(completion);
|
||||||
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);
|
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);
|
||||||
|
|||||||
@@ -2,18 +2,34 @@ package thb.jeanluc.adventure.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Onboarding walkthrough: an intro, ordered steps, and closing tips. */
|
/**
|
||||||
|
* Onboarding walkthrough: an intro, ordered steps, and closing tips.
|
||||||
|
*
|
||||||
|
* @param intro text shown before the first step; nullable
|
||||||
|
* @param steps the steps in the order the player must complete them
|
||||||
|
* @param closingTips text shown after the last step; nullable
|
||||||
|
*/
|
||||||
public record Tutorial(String intro, List<TutorialStep> steps, String closingTips) {
|
public record Tutorial(String intro, List<TutorialStep> steps, String closingTips) {
|
||||||
|
|
||||||
|
/** Normalises {@code steps} to an immutable, non-null list. */
|
||||||
public Tutorial {
|
public Tutorial {
|
||||||
steps = steps == null ? List.of() : List.copyOf(steps);
|
steps = steps == null ? List.of() : List.copyOf(steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this tutorial has anything to walk the player through.
|
||||||
|
*
|
||||||
|
* @return {@code true} if there are no steps
|
||||||
|
*/
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return steps.isEmpty();
|
return steps.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An absent tutorial (no steps). */
|
/**
|
||||||
|
* An absent tutorial (no steps).
|
||||||
|
*
|
||||||
|
* @return a tutorial that {@link #isEmpty()} reports as empty
|
||||||
|
*/
|
||||||
public static Tutorial none() {
|
public static Tutorial none() {
|
||||||
return new Tutorial(null, List.of(), null);
|
return new Tutorial(null, List.of(), null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ package thb.jeanluc.adventure.model;
|
|||||||
* ({@code expect} is a verb name like "look", or "go_direction"/"go_to_room"),
|
* ({@code expect} is a verb name like "look", or "go_direction"/"go_to_room"),
|
||||||
* a minimum argument count, a success confirmation, and a hint for a valid but
|
* a minimum argument count, a success confirmation, and a hint for a valid but
|
||||||
* wrong command.
|
* wrong command.
|
||||||
|
*
|
||||||
|
* @param instruction task text shown to the player while this step is active
|
||||||
|
* @param expect name of the command that advances this step
|
||||||
|
* @param minArgs number of arguments the command must carry to count
|
||||||
|
* @param confirm text written once the step is satisfied
|
||||||
|
* @param hint text written when the player runs a valid but different command
|
||||||
*/
|
*/
|
||||||
public record TutorialStep(
|
public record TutorialStep(
|
||||||
String instruction,
|
String instruction,
|
||||||
|
|||||||
@@ -40,19 +40,46 @@ public class World {
|
|||||||
/** Global lookup of combination recipes by canonical pair key. */
|
/** Global lookup of combination recipes by canonical pair key. */
|
||||||
private final Map<String, Combination> combinations;
|
private final Map<String, Combination> combinations;
|
||||||
|
|
||||||
/** Backward-compatible constructor for worlds without quests, endings, or combinations. */
|
/**
|
||||||
|
* Backward-compatible constructor for worlds without quests, endings, or combinations.
|
||||||
|
*
|
||||||
|
* @param rooms rooms by id
|
||||||
|
* @param items item prototypes by id
|
||||||
|
* @param npcs NPCs by id
|
||||||
|
* @param title display title
|
||||||
|
* @param welcomeMessage welcome message printed at startup
|
||||||
|
*/
|
||||||
public World(Map<String, Room> rooms, Map<String, Item> items, Map<String, Npc> npcs,
|
public World(Map<String, Room> rooms, Map<String, Item> items, Map<String, Npc> npcs,
|
||||||
String title, String welcomeMessage) {
|
String title, String welcomeMessage) {
|
||||||
this(rooms, items, npcs, title, welcomeMessage, Map.of(), List.of(), Map.of());
|
this(rooms, items, npcs, title, welcomeMessage, Map.of(), List.of(), Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Backward-compatible constructor for worlds with quests but no endings/combinations. */
|
/**
|
||||||
|
* Backward-compatible constructor for worlds with quests but no endings/combinations.
|
||||||
|
*
|
||||||
|
* @param rooms rooms by id
|
||||||
|
* @param items item prototypes by id
|
||||||
|
* @param npcs NPCs by id
|
||||||
|
* @param title display title
|
||||||
|
* @param welcomeMessage welcome message printed at startup
|
||||||
|
* @param quests quests by id
|
||||||
|
*/
|
||||||
public World(Map<String, Room> rooms, Map<String, Item> items, Map<String, Npc> npcs,
|
public World(Map<String, Room> rooms, Map<String, Item> items, Map<String, Npc> npcs,
|
||||||
String title, String welcomeMessage, Map<String, Quest> quests) {
|
String title, String welcomeMessage, Map<String, Quest> quests) {
|
||||||
this(rooms, items, npcs, title, welcomeMessage, quests, List.of(), Map.of());
|
this(rooms, items, npcs, title, welcomeMessage, quests, List.of(), Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Backward-compatible constructor for worlds with quests + endings but no combinations. */
|
/**
|
||||||
|
* Backward-compatible constructor for worlds with quests + endings but no combinations.
|
||||||
|
*
|
||||||
|
* @param rooms rooms by id
|
||||||
|
* @param items item prototypes by id
|
||||||
|
* @param npcs NPCs by id
|
||||||
|
* @param title display title
|
||||||
|
* @param welcomeMessage welcome message printed at startup
|
||||||
|
* @param quests quests by id
|
||||||
|
* @param endings endings in evaluation order
|
||||||
|
*/
|
||||||
public World(Map<String, Room> rooms, Map<String, Item> items, Map<String, Npc> npcs,
|
public World(Map<String, Room> rooms, Map<String, Item> items, Map<String, Npc> npcs,
|
||||||
String title, String welcomeMessage, Map<String, Quest> quests, List<Ending> endings) {
|
String title, String welcomeMessage, Map<String, Quest> quests, List<Ending> endings) {
|
||||||
this(rooms, items, npcs, title, welcomeMessage, quests, endings, Map.of());
|
this(rooms, items, npcs, title, welcomeMessage, quests, endings, Map.of());
|
||||||
|
|||||||
@@ -20,10 +20,16 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public final class SaveCodec {
|
public final class SaveCodec {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private SaveCodec() {
|
private SaveCodec() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reads the mutable state of {@code session} into a {@link SaveData}. */
|
/**
|
||||||
|
* Reads the mutable state of {@code session} into a {@link SaveData}.
|
||||||
|
*
|
||||||
|
* @param session the live session to snapshot
|
||||||
|
* @return a snapshot of the session's mutable state
|
||||||
|
*/
|
||||||
public static SaveData capture(GameSession session) {
|
public static SaveData capture(GameSession session) {
|
||||||
World w = session.getWorld();
|
World w = session.getWorld();
|
||||||
Player p = session.getPlayer();
|
Player p = session.getPlayer();
|
||||||
@@ -67,6 +73,10 @@ public final class SaveCodec {
|
|||||||
* Overlays {@code data} onto {@code session} (whose world was just loaded
|
* Overlays {@code data} onto {@code session} (whose world was just loaded
|
||||||
* from YAML). Throws {@link SaveException} if the save references ids that
|
* from YAML). Throws {@link SaveException} if the save references ids that
|
||||||
* no longer exist in the current world.
|
* no longer exist in the current world.
|
||||||
|
*
|
||||||
|
* @param data the snapshot to restore
|
||||||
|
* @param session the session to overwrite
|
||||||
|
* @throws SaveException if the snapshot refers to a room or item the current world does not contain
|
||||||
*/
|
*/
|
||||||
public static void apply(SaveData data, GameSession session) {
|
public static void apply(SaveData data, GameSession session) {
|
||||||
World w = session.getWorld();
|
World w = session.getWorld();
|
||||||
@@ -106,6 +116,7 @@ public final class SaveCodec {
|
|||||||
session.setTurn(data.turn());
|
session.setTurn(data.turn());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolves a room id against the world, or fails with a {@link SaveException}. */
|
||||||
private static Room requireRoom(World w, String id) {
|
private static Room requireRoom(World w, String id) {
|
||||||
Room r = w.getRooms().get(id);
|
Room r = w.getRooms().get(id);
|
||||||
if (r == null) {
|
if (r == null) {
|
||||||
@@ -115,6 +126,7 @@ public final class SaveCodec {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolves an item id against the world, or fails with a {@link SaveException}. */
|
||||||
private static Item requireItem(World w, String id) {
|
private static Item requireItem(World w, String id) {
|
||||||
Item i = w.getItems().get(id);
|
Item i = w.getItems().get(id);
|
||||||
if (i == null) {
|
if (i == null) {
|
||||||
|
|||||||
@@ -7,6 +7,21 @@ import java.util.Map;
|
|||||||
* JSON-serialised snapshot of the mutable game state — a delta laid over the
|
* JSON-serialised snapshot of the mutable game state — a delta laid over the
|
||||||
* world freshly loaded from YAML. Header fields ({@code slotName}, room,
|
* world freshly loaded from YAML. Header fields ({@code slotName}, room,
|
||||||
* {@code turn}, {@code savedAtEpochMillis}) double as the slot-list metadata.
|
* {@code turn}, {@code savedAtEpochMillis}) double as the slot-list metadata.
|
||||||
|
*
|
||||||
|
* @param schemaVersion on-disk format version; must equal {@link #CURRENT_VERSION} to load
|
||||||
|
* @param worldTitle title of the world the snapshot was taken from
|
||||||
|
* @param slotName player-visible name of the save slot
|
||||||
|
* @param savedAtEpochMillis wall-clock time the snapshot was written, in epoch milliseconds
|
||||||
|
* @param turn turn counter at the time of saving
|
||||||
|
* @param currentRoomId id of the room the player stands in
|
||||||
|
* @param visitedRoomIds ids of all rooms the player has entered (fog of war)
|
||||||
|
* @param gold the player's gold amount
|
||||||
|
* @param inventoryItemIds ids of the items the player carries
|
||||||
|
* @param flags set world-state flags
|
||||||
|
* @param questStages stage index per active quest id
|
||||||
|
* @param questCompleted ids of completed quests
|
||||||
|
* @param roomItemIds item ids lying in each room, keyed by room id; rooms without items are omitted
|
||||||
|
* @param switchStates on/off state per switchable item id
|
||||||
*/
|
*/
|
||||||
public record SaveData(
|
public record SaveData(
|
||||||
int schemaVersion,
|
int schemaVersion,
|
||||||
|
|||||||
@@ -3,15 +3,30 @@ package thb.jeanluc.adventure.save;
|
|||||||
/** Thrown when a save cannot be written/read; carries a player-facing message. */
|
/** Thrown when a save cannot be written/read; carries a player-facing message. */
|
||||||
public class SaveException extends RuntimeException {
|
public class SaveException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a failure that keeps the triggering exception as its cause.
|
||||||
|
*
|
||||||
|
* @param message player-facing description of the failure
|
||||||
|
* @param cause the underlying I/O or parse failure
|
||||||
|
*/
|
||||||
public SaveException(String message, Throwable cause) {
|
public SaveException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a failure without an underlying cause.
|
||||||
|
*
|
||||||
|
* @param message player-facing description of the failure
|
||||||
|
*/
|
||||||
public SaveException(String message) {
|
public SaveException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Player-facing message (the constructor message is already user-friendly). */
|
/**
|
||||||
|
* Player-facing message (the constructor message is already user-friendly).
|
||||||
|
*
|
||||||
|
* @return the message to show to the player
|
||||||
|
*/
|
||||||
public String getUserMessage() {
|
public String getUserMessage() {
|
||||||
return getMessage();
|
return getMessage();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
package thb.jeanluc.adventure.save;
|
package thb.jeanluc.adventure.save;
|
||||||
|
|
||||||
/** Lightweight metadata for one save slot, shown in the Load menu. */
|
/**
|
||||||
|
* Lightweight metadata for one save slot, shown in the Load menu.
|
||||||
|
*
|
||||||
|
* @param slug filename stem of the slot on disk; the id used to load it
|
||||||
|
* @param slotName player-visible name of the slot
|
||||||
|
* @param roomId id of the room the player stands in
|
||||||
|
* @param turn turn counter at the time of saving
|
||||||
|
* @param savedAtEpochMillis wall-clock time the slot was written, in epoch milliseconds
|
||||||
|
*/
|
||||||
public record SaveSlotInfo(String slug, String slotName, String roomId,
|
public record SaveSlotInfo(String slug, String slotName, String roomId,
|
||||||
int turn, long savedAtEpochMillis) {
|
int turn, long savedAtEpochMillis) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,16 @@ package thb.jeanluc.adventure.save;
|
|||||||
import thb.jeanluc.adventure.io.ConsoleIO;
|
import thb.jeanluc.adventure.io.ConsoleIO;
|
||||||
import thb.jeanluc.adventure.io.MusicLevel;
|
import thb.jeanluc.adventure.io.MusicLevel;
|
||||||
|
|
||||||
/** Persisted user preferences (colour + glyph fidelity + music level). */
|
/**
|
||||||
|
* Persisted user preferences (colour + glyph fidelity + music level).
|
||||||
|
*
|
||||||
|
* @param colorMode ANSI colour policy of the console frontend
|
||||||
|
* @param glyphMode glyph fidelity used when rendering maps and frames
|
||||||
|
* @param musicLevel volume level of the background music
|
||||||
|
*/
|
||||||
public record Settings(ConsoleIO.ColorMode colorMode, ConsoleIO.GlyphMode glyphMode, MusicLevel musicLevel) {
|
public record Settings(ConsoleIO.ColorMode colorMode, ConsoleIO.GlyphMode glyphMode, MusicLevel musicLevel) {
|
||||||
|
|
||||||
|
/** Compact canonical constructor: substitutes the defaults for any null component. */
|
||||||
public Settings {
|
public Settings {
|
||||||
if (colorMode == null) {
|
if (colorMode == null) {
|
||||||
colorMode = ConsoleIO.ColorMode.AUTO;
|
colorMode = ConsoleIO.ColorMode.AUTO;
|
||||||
@@ -18,11 +25,21 @@ public record Settings(ConsoleIO.ColorMode colorMode, ConsoleIO.GlyphMode glyphM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Backward-compatible constructor for callers that don't set music. */
|
/**
|
||||||
|
* Backward-compatible constructor for callers that don't set music.
|
||||||
|
*
|
||||||
|
* @param colorMode ANSI colour policy of the console frontend
|
||||||
|
* @param glyphMode glyph fidelity used when rendering maps and frames
|
||||||
|
*/
|
||||||
public Settings(ConsoleIO.ColorMode colorMode, ConsoleIO.GlyphMode glyphMode) {
|
public Settings(ConsoleIO.ColorMode colorMode, ConsoleIO.GlyphMode glyphMode) {
|
||||||
this(colorMode, glyphMode, MusicLevel.MEDIUM);
|
this(colorMode, glyphMode, MusicLevel.MEDIUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings used when nothing has been persisted yet.
|
||||||
|
*
|
||||||
|
* @return the default preferences
|
||||||
|
*/
|
||||||
public static Settings defaults() {
|
public static Settings defaults() {
|
||||||
return new Settings(ConsoleIO.ColorMode.AUTO, ConsoleIO.GlyphMode.UNICODE, MusicLevel.MEDIUM);
|
return new Settings(ConsoleIO.ColorMode.AUTO, ConsoleIO.GlyphMode.UNICODE, MusicLevel.MEDIUM);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,18 +11,31 @@ import java.nio.file.Path;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class SettingsStore {
|
public class SettingsStore {
|
||||||
|
|
||||||
|
/** Backing JSON file. */
|
||||||
private final Path file;
|
private final Path file;
|
||||||
|
|
||||||
|
/** Jackson mapper used for both directions. */
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/** Uses the default {@code saves/settings.json} file. */
|
||||||
public SettingsStore() {
|
public SettingsStore() {
|
||||||
this(Path.of("saves", "settings.json"));
|
this(Path.of("saves", "settings.json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses a custom settings file.
|
||||||
|
*
|
||||||
|
* @param file JSON file to read from and write to
|
||||||
|
*/
|
||||||
public SettingsStore(Path file) {
|
public SettingsStore(Path file) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns persisted settings, or {@link Settings#defaults()} on any problem. */
|
/**
|
||||||
|
* Returns persisted settings, or {@link Settings#defaults()} on any problem.
|
||||||
|
*
|
||||||
|
* @return the stored settings, or the defaults if the file is missing or unreadable
|
||||||
|
*/
|
||||||
public Settings load() {
|
public Settings load() {
|
||||||
if (!Files.isRegularFile(file)) {
|
if (!Files.isRegularFile(file)) {
|
||||||
return Settings.defaults();
|
return Settings.defaults();
|
||||||
@@ -35,7 +48,11 @@ public class SettingsStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Persists settings; logs and swallows failures (non-fatal). */
|
/**
|
||||||
|
* Persists settings; logs and swallows failures (non-fatal).
|
||||||
|
*
|
||||||
|
* @param settings the preferences to write
|
||||||
|
*/
|
||||||
public void save(Settings settings) {
|
public void save(Settings settings) {
|
||||||
try {
|
try {
|
||||||
if (file.getParent() != null) {
|
if (file.getParent() != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user