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:
2026-07-14 12:39:25 +02:00
parent 1b39eac90e
commit 526e3d4ee7
75 changed files with 1316 additions and 102 deletions

View File

@@ -16,10 +16,17 @@ import java.util.List;
@RequiredArgsConstructor
public class MenuCommand implements Command {
/** Persistence backend the active slot is written to before leaving. */
private final SaveService saves;
/** Running game whose loop is stopped; null until {@link #bind(Game)} is called. */
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) {
this.game = game;
}

View File

@@ -12,6 +12,7 @@ import java.util.List;
@RequiredArgsConstructor
public class SaveCommand implements Command {
/** Persistence backend the active slot is written to. */
private final SaveService saves;
@Override

View File

@@ -10,6 +10,13 @@ public final class 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) {
return switch (c.type()) {
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) {
if (conditions == null) {
return true;

View File

@@ -13,6 +13,13 @@ public final class 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) {
switch (e.type()) {
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) {
if (effects == null) {
return;

View File

@@ -9,7 +9,12 @@ public final class 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) {
for (Ending e : ctx.getWorld().getEndings()) {
if (Conditions.all(e.when(), ctx)) {
@@ -19,7 +24,14 @@ public final class EndingEngine {
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) {
int total = ctx.getWorld().getQuests().size();
int done = ctx.getQuestLog().completed().size();
@@ -34,6 +46,7 @@ public final class EndingEngine {
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) {
if (e.victory() && total > 0 && done == total) {
return "Master of the Manor";

View File

@@ -39,7 +39,13 @@ public final class 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) {
}
@@ -87,6 +93,11 @@ public final class EscortEngine {
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) {
if (ctx.getState().isSet(REUNITED_FLAG)) {
return;
@@ -107,6 +118,7 @@ public final class EscortEngine {
.build());
}
/** Adds the reward item to the inventory unless the player already carries it. */
private static void grantReward(GameContext ctx) {
Item reward = ctx.getWorld().getItems().get(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());
}
/** 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) {
Npc npc = ctx.getWorld().getNpcs().get(twin.npcId());
return npc != null ? npc.getName() : twin.npcId();

View File

@@ -11,47 +11,96 @@ import thb.jeanluc.adventure.model.World;
*/
public class GameContext {
/** The mutable state of the current playthrough. */
private final GameSession session;
/** Channel commands read input from and write output to. */
private final GameIO io;
/** Silent autosave hook, wired by the app; no-op by default (tests). */
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) {
this.session = session;
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) {
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() {
return session;
}
/**
* Delegates to the session.
*
* @return the loaded world
*/
public World getWorld() {
return session.getWorld();
}
/**
* Delegates to the session.
*
* @return the player character
*/
public Player getPlayer() {
return session.getPlayer();
}
/**
* Delegates to the session.
*
* @return the world flags
*/
public GameState getState() {
return session.getState();
}
/**
* Delegates to the session.
*
* @return the quest progress
*/
public QuestLog getQuestLog() {
return session.getQuestLog();
}
/**
* Returns the channel commands read from and write to.
*
* @return the IO channel
*/
public GameIO getIo() {
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) {
this.saveCallback = saveCallback;
}

View File

@@ -13,23 +13,47 @@ import thb.jeanluc.adventure.model.World;
@Getter
public class GameSession {
/** The loaded world this playthrough runs on. */
private final World world;
/** The player character, including inventory, position, and visited rooms. */
private final Player player;
/** World flags set and cleared by effects and conditions. */
private final GameState state = new GameState();
/** Quest progress: active stages and completed quests. */
private final QuestLog questLog = new QuestLog();
/** Name of the save slot this session is bound to; save/load target. */
private final String slotName;
/** Number of commands executed so far; shown in the HUD and the ending summary. */
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) {
this.world = world;
this.player = player;
this.slotName = slotName;
}
/**
* Overwrites the turn counter (used when restoring a save).
*
* @param turn the turn count to restore
*/
public void setTurn(int turn) {
this.turn = turn;
}
/** Advances the turn counter by one. */
public void incrementTurn() {
this.turn++;
}

View File

@@ -8,25 +8,51 @@ import java.util.Set;
/** Named world flags. A flag is "set" (true) when present in the set. */
public class GameState {
/** Ids of the currently set flags. Insertion-ordered for stable saves. */
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) {
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) {
flags.add(flag);
}
/**
* Clears a flag. Clearing an unset flag has no effect.
*
* @param flag the flag id
*/
public void clear(String flag) {
flags.remove(flag);
}
/**
* All currently set flags.
*
* @return an unmodifiable view of the set flags
*/
public Set<String> all() {
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) {
flags.clear();
flags.addAll(newFlags);

View File

@@ -16,6 +16,15 @@ public final class 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) {
Map<String, Quest> quests = ctx.getWorld().getQuests();
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) {
if (ctx.getQuestLog().isCompleted(q.id())) {
return;
@@ -65,6 +75,13 @@ public final class QuestEngine {
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) {
Map<String, Quest> quests = ctx.getWorld().getQuests();
List<QuestEntry> active = new ArrayList<>();
@@ -85,6 +102,7 @@ public final class QuestEngine {
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) {
int total = 0;
for (Quest q : quests.values()) {

View File

@@ -10,45 +10,97 @@ import java.util.Set;
/** Runtime quest progress: current stage per active quest, plus completed ids. */
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<>();
/** Ids of quests that have been completed. Insertion-ordered for stable saves. */
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) {
if (!completed.contains(id)) {
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) {
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) {
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) {
return stageIndex.getOrDefault(id, 0);
}
/**
* Moves a quest on to its next stage.
*
* @param id the quest id
*/
public void advance(String id) {
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) {
stageIndex.remove(id);
completed.add(id);
}
/**
* Ids of all currently active quests.
*
* @return an unmodifiable view of the active quest ids
*/
public Set<String> active() {
return Collections.unmodifiableSet(stageIndex.keySet());
}
/**
* Ids of all completed quests.
*
* @return an unmodifiable view of the completed quest ids
*/
public Set<String> 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) {
stageIndex.clear();
stageIndex.putAll(stages);

View File

@@ -18,25 +18,51 @@ import java.util.Optional;
*/
public final class TutorialGuide {
/** The walkthrough content: intro, steps, and closing tips. */
private final Tutorial tutorial;
/** Command lookup, used to compare a typed verb against a step's expected command via aliases. */
private final CommandRegistry registry;
/** Index of the step currently being taught. */
private int index;
/** False once all steps are done or the walkthrough was skipped. */
private boolean active;
/** True once {@link #begin(GameContext)} has printed the intro; guards against a second run. */
private boolean begun;
/** Room the player stood in when the current step began; used to detect movement steps. */
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) {
this.tutorial = tutorial;
this.registry = registry;
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() {
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) {
if (!active || begun) {
return;
@@ -48,7 +74,14 @@ public final class TutorialGuide {
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) {
if (!active) {
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) {
if (active) {
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) {
roomAtStepStart = ctx.getPlayer().getCurrentRoom();
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) {
String expect = step.expect();
if ("go_direction".equals(expect)) {
@@ -98,6 +142,7 @@ public final class TutorialGuide {
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) {
if (p.args().isEmpty()) {
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) {
return roomAtStepStart != null
&& 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) {
Optional<Command> ca = registry.find(a);
Optional<Command> cb = registry.find(b);

View File

@@ -23,28 +23,70 @@ import java.util.List;
public class ConsoleIO implements GameIO {
/** 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. */
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;
/** Source the player's commands are read from. */
private final BufferedReader in;
/** Sink all output is written to. */
private final PrintStream out;
/** Whether ANSI colour codes are currently emitted; resolved from a {@link ColorMode}. */
private boolean useColor;
/** Frame/glyph fidelity tier currently in use. */
private GlyphMode glyphs;
/** Reads from {@code System.in} and writes to {@code System.out}, colour auto-detected. */
public ConsoleIO() {
this(new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)),
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) {
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) {
this.in = in;
this.out = out;
@@ -52,7 +94,11 @@ public class ConsoleIO implements GameIO {
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) {
this.useColor = switch (mode) {
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) {
this.glyphs = mode;
}
@@ -118,6 +168,14 @@ public class ConsoleIO implements GameIO {
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) {
StringBuilder sb = new StringBuilder(" ").append(paint(Style.DIM, label)).append(" ");
for (int i = 0; i < xs.size(); i++) {
@@ -129,6 +187,10 @@ public class ConsoleIO implements GameIO {
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) {
String tl = glyphs == GlyphMode.ASCII ? "+" : "";
String tr = glyphs == GlyphMode.ASCII ? "+" : "";
@@ -141,6 +203,9 @@ public class ConsoleIO implements GameIO {
return sb.append(tr).toString();
}
/**
* @return the bottom border of a room frame, {@link #FRAME_WIDTH} characters wide
*/
private String frameBottom() {
String bl = glyphs == GlyphMode.ASCII ? "+" : "";
String br = glyphs == GlyphMode.ASCII ? "+" : "";
@@ -152,6 +217,13 @@ public class ConsoleIO implements GameIO {
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) {
if (!useColor) {
return s;

View File

@@ -18,28 +18,52 @@ import java.util.List;
*/
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();
/** 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);
/** Convenience: a plain, unstyled line. */
/**
* Convenience: a plain, unstyled line.
*
* @param s the text to write
*/
default void write(String 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) {
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) {
// 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) {
// no-op
}
@@ -55,27 +79,47 @@ public interface GameIO {
// 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) {
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) {
// 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) {
// 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) {
// 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) {
print(QuestText.render(view));
}

View File

@@ -31,30 +31,64 @@ import java.util.List;
*/
public class MapPanel extends JComponent {
/** Unscaled room width, in pixels. */
private static final int BASE_CELL_W = 96;
/** Unscaled room height, in pixels. */
private static final int BASE_CELL_H = 46;
/** Unscaled horizontal gap between two rooms, in pixels. */
private static final int BASE_GAP_X = 34;
/** Unscaled vertical gap between two rooms, in pixels. */
private static final int BASE_GAP_Y = 30;
/** Unscaled padding around the whole map, in pixels. */
private static final int BASE_PAD = 16;
/** Unscaled size of the room-label font, in points. */
private static final float BASE_FONT = 11f;
/** Panel background. */
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);
/** Outline colour of a {@link CellState#VISITED} room. */
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);
/** Outline colour of a {@link CellState#KNOWN} but unvisited room. */
private static final Color KNOWN = new Color(0x3a, 0x46, 0x55);
/** Font the room labels are derived from. */
private final Font baseFont;
/** User zoom multiplier, applied on top of the fit-to-panel scale. */
private float zoom = 1f;
/** The map currently drawn; empty until the first {@link #show(MapView)}. */
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) {
this.baseFont = font;
setBackground(BG);
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) {
SwingUtilities.invokeLater(() -> {
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) {
SwingUtilities.invokeLater(() -> {
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() {
Container p = getParent();
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 the highest occupied column and row of the map grid, as
* {@code [maxX, maxY]}
*/
private int[] gridBounds() {
int maxX = 0;
int maxY = 0;
@@ -97,7 +143,13 @@ public class MapPanel extends JComponent {
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) {
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;
@@ -109,6 +161,11 @@ public class MapPanel extends JComponent {
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) {
int pad = Math.round(BASE_PAD * eff);
int cw = Math.round(BASE_CELL_W * eff);

View File

@@ -3,13 +3,22 @@ package thb.jeanluc.adventure.io;
/** Raw audio operations — the only part that touches sound hardware. */
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);
/** Fade the current track out and stop. */
void fadeOut();
/** Set the playback volume immediately. */
/**
* Set the playback volume immediately.
*
* @param gainDb target volume in decibels
*/
void setGainDb(float gainDb);
/** Stop playback and release resources. */

View File

@@ -10,15 +10,31 @@ import java.util.Objects;
*/
public final class MusicController {
/** Backend the raw playback operations are delegated to. */
private final MusicBackend backend;
/** Volume level currently selected in the Settings menu. */
private MusicLevel level = MusicLevel.MEDIUM;
/** Track currently selected, or null when silent. */
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) {
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) {
if (!level.isOn()) {
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) {
if (newLevel == this.level) {
return;
@@ -49,6 +71,7 @@ public final class MusicController {
}
}
/** Stops playback and releases the backend's resources. */
public void shutdown() {
backend.shutdown();
}

View File

@@ -2,9 +2,23 @@ package thb.jeanluc.adventure.io;
/** Background-music volume level, cycled in the Settings menu. */
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() {
return switch (this) {
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() {
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() {
return values()[(ordinal() + 1) % values().length];
}

View File

@@ -18,6 +18,11 @@ import java.awt.Insets;
/** Read-only styled view of the quest log, shown under the map. */
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) {
setEditable(false);
setBackground(new Color(0x0b, 0x0e, 0x13));
@@ -26,6 +31,11 @@ public class QuestPanel extends JTextPane {
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) {
SwingUtilities.invokeLater(() -> {
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) {
return switch (s) {
case HEADING -> new Color(0xc9, 0x8a, 0xe0);

View File

@@ -11,13 +11,26 @@ import java.util.Set;
*/
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
/** Upper bound for the room-name field width, in characters. */
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
/** Utility class; not instantiable. */
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) {
if (view.cells().isEmpty()) {
return StyledText.of("(no map yet)");
@@ -98,6 +111,13 @@ public final class AsciiMap {
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) {
switch (state) {
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) {
String n = c.name();
if (n.length() > inner) {
@@ -114,6 +139,11 @@ public final class AsciiMap {
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) {
int total = width - s.length();
int left = total / 2;
@@ -121,6 +151,11 @@ public final class AsciiMap {
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) {
return (((long) x) << 32) ^ (y & 0xffffffffL);
}

View File

@@ -3,10 +3,16 @@ package thb.jeanluc.adventure.io.text;
/** Builds styled banners for big moments (game start, finale). */
public final class Banner {
/** Utility class; not instantiable. */
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) {
String bar = "=".repeat(Math.max(8, title.length() + 8));
return StyledText.builder()

View File

@@ -1,4 +1,14 @@
package thb.jeanluc.adventure.io.text;
/** 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
}

View File

@@ -1,4 +1,9 @@
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) {}

View File

@@ -1,4 +1,11 @@
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) {}

View File

@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.io.text;
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) {
/** Defensively copies both lists, making the snapshot immutable. */
public MapView {
cells = List.copyOf(cells);
connections = List.copyOf(connections);

View File

@@ -1,4 +1,9 @@
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) {}

View File

@@ -3,9 +3,17 @@ package thb.jeanluc.adventure.io.text;
/** Renders a {@link QuestView} as styled text for the console quest log. */
public final class QuestText {
/** Utility class; not instantiable. */
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) {
if (view.active().isEmpty() && view.completed().isEmpty()) {
return StyledText.of("You have no quests yet.");

View File

@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.io.text;
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) {
/** Defensively copies both lists, making the snapshot immutable. */
public QuestView {
active = List.copyOf(active);
completed = List.copyOf(completed);

View File

@@ -5,10 +5,16 @@ import java.util.List;
/** Default, layout-free rendering of high-level views into {@link StyledText}. */
public final class Renderings {
/** Utility class; not instantiable. */
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) {
StyledText.Builder b = StyledText.builder();
b.heading(r.name()).plain("\n");
@@ -33,6 +39,13 @@ public final class Renderings {
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) {
for (int i = 0; i < xs.size(); i++) {
if (i > 0) {

View File

@@ -1,4 +1,12 @@
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) {}

View File

@@ -2,9 +2,18 @@ package thb.jeanluc.adventure.io.text;
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,
List<String> items, List<String> npcs, List<String> exits) {
/** Defensively copies the lists, making the snapshot immutable. */
public RoomView {
items = List.copyOf(items);
npcs = List.copyOf(npcs);

View File

@@ -1,7 +1,17 @@
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) {
/**
* Rejects null components.
*
* @throws IllegalArgumentException if {@code text} or {@code style} is null
*/
public Span {
if (text == null) {
throw new IllegalArgumentException("text must not be null");

View File

@@ -1,4 +1,26 @@
package thb.jeanluc.adventure.io.text;
/** 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
}

View File

@@ -6,18 +6,30 @@ import java.util.List;
/** An ordered, immutable list of styled {@link Span}s. Built via {@link Builder}. */
public final class StyledText {
/** The spans of this text, in output order; unmodifiable. */
private final List<Span> spans;
/**
* @param spans the spans to copy into this text
*/
private StyledText(List<Span> 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() {
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() {
StringBuilder sb = new StringBuilder();
for (Span s : spans) {
@@ -26,32 +38,104 @@ public final class StyledText {
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) {
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() {
return new Builder();
}
/** Fluent builder; one method per {@link Style} role. */
public static final class Builder {
/** Spans collected so far, in the order the append methods were called. */
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) {
spans.add(new Span(text, style));
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); }
/**
* 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); }
/**
* 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); }
/**
* 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); }
/**
* 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); }
/**
* 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); }
/**
* 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); }
/**
* Finishes the build; the builder may not be reused meaningfully afterwards.
*
* @return an immutable styled text of the collected spans
*/
public StyledText build() {
return new StyledText(spans);
}

View File

@@ -12,9 +12,19 @@ import java.util.Map;
/** Builds {@link Combination}s from {@link CombinationDto}s, validating item ids. */
public final class CombinationFactory {
/** Utility class; not instantiable. */
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) {
requireItem(items, dto.a(), "a");
requireItem(items, dto.b(), "b");
@@ -41,6 +51,14 @@ public final class CombinationFactory {
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) {
if (id == null) {
throw new WorldLoadException("Combination '" + field + "' is missing an item id");

View File

@@ -7,9 +7,17 @@ import thb.jeanluc.adventure.model.Ending;
/** Builds {@link Ending} objects from {@link EndingDto}s. */
public final class EndingFactory {
/** Utility class; not instantiable. */
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) {
return new Ending(
dto.id(),

View File

@@ -9,6 +9,7 @@ import thb.jeanluc.adventure.model.Npc;
*/
public final class NpcFactory {
/** Utility class; not instantiable. */
private NpcFactory() {
}

View File

@@ -13,9 +13,17 @@ import java.util.List;
/** Builds {@link Quest} objects from {@link QuestDto}s (no reference resolution needed). */
public final class QuestFactory {
/** Utility class; not instantiable. */
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) {
List<QuestStage> stages = new ArrayList<>();
if (dto.stages() != null) {

View File

@@ -9,6 +9,7 @@ import thb.jeanluc.adventure.model.Room;
*/
public final class RoomFactory {
/** Utility class; not instantiable. */
private RoomFactory() {
}

View File

@@ -22,18 +22,33 @@ public class TutorialLoader {
/** Default classpath base directory. */
public static final String DEFAULT_BASE = "/world";
/** Single Jackson mapper, configured once for YAML input. */
private final ObjectMapper yaml = new ObjectMapper(new YAMLFactory());
/** Classpath base directory (typically {@link #DEFAULT_BASE} or a test override). */
private final String basePath;
/** Convenience constructor using {@link #DEFAULT_BASE}. */
public TutorialLoader() {
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) {
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() {
String resource = basePath + "/tutorial.yaml";
try (InputStream in = getClass().getResourceAsStream(resource)) {

View File

@@ -2,7 +2,18 @@ package thb.jeanluc.adventure.loader.dto;
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(
String a,
String b,

View File

@@ -6,9 +6,21 @@ import thb.jeanluc.adventure.model.Condition;
import java.util.ArrayList;
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) {
/**
* Converts this DTO into its domain condition.
*
* @return the matching {@link Condition}
* @throws WorldLoadException if none of the fields is set
*/
public Condition toModel() {
if (flag != null) {
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");
}
/**
* 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) {
List<Condition> out = new ArrayList<>();
if (dtos != null) {

View File

@@ -2,6 +2,11 @@ package thb.jeanluc.adventure.loader.dto;
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) {
}

View File

@@ -2,6 +2,11 @@ package thb.jeanluc.adventure.loader.dto;
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) {
}

View File

@@ -6,15 +6,38 @@ import thb.jeanluc.adventure.model.Effect;
import java.util.ArrayList;
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,
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) {
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() {
if (setFlag != null) {
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");
}
/**
* 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) {
List<Effect> out = new ArrayList<>();
if (dtos != null) {

View File

@@ -2,6 +2,14 @@ package thb.jeanluc.adventure.loader.dto;
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) {
}

View File

@@ -2,6 +2,12 @@ package thb.jeanluc.adventure.loader.dto;
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) {
}

View File

@@ -20,7 +20,15 @@ public record NpcDto(
List<ReactionDto> reactions,
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) {
this(id, name, description, greeting, reactions, null);
}

View File

@@ -2,7 +2,15 @@ package thb.jeanluc.adventure.loader.dto;
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,
List<QuestStageDto> stages, List<EffectDto> onComplete) {
}

View File

@@ -2,6 +2,12 @@ package thb.jeanluc.adventure.loader.dto;
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) {
}

View File

@@ -20,7 +20,14 @@ public record ReactionDto(
List<ConditionDto> requires,
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) {
this(onReceive, response, gives, consumes, null, null);
}

View File

@@ -29,7 +29,16 @@ public record RoomDto(
Boolean dark,
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,
Map<String, String> exits, List<String> items, List<String> npcs) {
this(id, name, description, exits, items, npcs, null, null, null, null);

View File

@@ -2,6 +2,12 @@ package thb.jeanluc.adventure.loader.dto;
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) {
}

View File

@@ -1,6 +1,14 @@
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(
String instruction,
String expect,

View File

@@ -10,17 +10,29 @@ import java.util.List;
/** Frontend-agnostic main menu, slot picker, and new-game name prompt. */
public final class MainMenu {
/** Utility class; not instantiable. */
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) {
int i = io.choose("HAUNTED MANOR",
List.of("New Game", "Load Game", "Settings", "Quit"));
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) {
io.write("Name your save (Enter for \"" + defaultName + "\"):");
String line = io.readLine();
@@ -31,6 +43,10 @@ public final class MainMenu {
* Lets the player pick a save slot to load. Returns the chosen
* {@link SaveSlotInfo}, or {@code null} if there are none or the player
* 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) {
List<SaveSlotInfo> slots = saves.list();

View File

@@ -2,5 +2,16 @@ package thb.jeanluc.adventure.menu;
/** Top-level main-menu choices, in display order. */
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
}

View File

@@ -10,10 +10,18 @@ import java.util.List;
/** Settings screen: cycle colour, glyph, and music level; persists + applies. */
public final class SettingsMenu {
/** Utility class; not instantiable. */
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) {
Settings s = current;
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) {
io.setMusicLevel(s.musicLevel());
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) {
return switch (m) {
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) {
return switch (m) {
case UNICODE -> ConsoleIO.GlyphMode.ASCII;

View File

@@ -6,6 +6,15 @@ import java.util.List;
* A data-driven item-combination recipe. Identified by an unordered pair of
* item ids ({@code a}, {@code b}). When applied it may require conditions,
* 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(
String a,
@@ -17,18 +26,29 @@ public record Combination(
String response,
String failText
) {
/** Normalises the list components to immutable, non-null lists. */
public Combination {
requires = requires == null ? List.of() : List.copyOf(requires);
consume = consume == null ? List.of() : List.copyOf(consume);
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) {
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() {
return key(a, b);
}

View File

@@ -1,6 +1,21 @@
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 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
}
}

View File

@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.model;
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) {
/** Normalises {@code when} to an immutable, non-null list. */
public DescriptionState {
when = when == null ? List.of() : List.copyOf(when);
}

View File

@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.model;
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) {
/** Normalises {@code when} to an immutable, non-null list. */
public DialogueLine {
when = when == null ? List.of() : List.copyOf(when);
}

View File

@@ -1,6 +1,29 @@
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 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
}
}

View File

@@ -2,8 +2,17 @@ package thb.jeanluc.adventure.model;
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) {
/** Normalises {@code when} to an immutable, non-null list. */
public Ending {
when = when == null ? List.of() : List.copyOf(when);
}

View File

@@ -2,8 +2,14 @@ package thb.jeanluc.adventure.model;
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) {
/** Normalises {@code requires} to an immutable, non-null list. */
public ExitLock {
requires = requires == null ? List.of() : List.copyOf(requires);
}

View File

@@ -2,9 +2,19 @@ package thb.jeanluc.adventure.model;
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,
List<QuestStage> stages, List<Effect> onComplete) {
/** Normalises the list components to immutable, non-null lists. */
public Quest {
stages = stages == null ? List.of() : List.copyOf(stages);
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);

View File

@@ -2,8 +2,15 @@ package thb.jeanluc.adventure.model;
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) {
/** Normalises the list components to immutable, non-null lists. */
public QuestStage {
completion = completion == null ? List.of() : List.copyOf(completion);
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);

View File

@@ -2,18 +2,34 @@ package thb.jeanluc.adventure.model;
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) {
/** Normalises {@code steps} to an immutable, non-null list. */
public Tutorial {
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() {
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() {
return new Tutorial(null, List.of(), null);
}

View File

@@ -5,6 +5,12 @@ package thb.jeanluc.adventure.model;
* ({@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
* 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(
String instruction,

View File

@@ -40,19 +40,46 @@ public class World {
/** Global lookup of combination recipes by canonical pair key. */
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,
String title, String welcomeMessage) {
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,
String title, String welcomeMessage, Map<String, Quest> quests) {
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,
String title, String welcomeMessage, Map<String, Quest> quests, List<Ending> endings) {
this(rooms, items, npcs, title, welcomeMessage, quests, endings, Map.of());

View File

@@ -20,10 +20,16 @@ import java.util.Map;
*/
public final class SaveCodec {
/** Utility class; not instantiable. */
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) {
World w = session.getWorld();
Player p = session.getPlayer();
@@ -67,6 +73,10 @@ public final class SaveCodec {
* Overlays {@code data} onto {@code session} (whose world was just loaded
* from YAML). Throws {@link SaveException} if the save references ids that
* 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) {
World w = session.getWorld();
@@ -106,6 +116,7 @@ public final class SaveCodec {
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) {
Room r = w.getRooms().get(id);
if (r == null) {
@@ -115,6 +126,7 @@ public final class SaveCodec {
return r;
}
/** Resolves an item id against the world, or fails with a {@link SaveException}. */
private static Item requireItem(World w, String id) {
Item i = w.getItems().get(id);
if (i == null) {

View File

@@ -7,6 +7,21 @@ import java.util.Map;
* JSON-serialised snapshot of the mutable game state — a delta laid over the
* world freshly loaded from YAML. Header fields ({@code slotName}, room,
* {@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(
int schemaVersion,

View File

@@ -3,15 +3,30 @@ package thb.jeanluc.adventure.save;
/** Thrown when a save cannot be written/read; carries a player-facing message. */
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) {
super(message, cause);
}
/**
* Creates a failure without an underlying cause.
*
* @param message player-facing description of the failure
*/
public SaveException(String 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() {
return getMessage();
}

View File

@@ -1,6 +1,14 @@
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,
int turn, long savedAtEpochMillis) {
}

View File

@@ -3,9 +3,16 @@ package thb.jeanluc.adventure.save;
import thb.jeanluc.adventure.io.ConsoleIO;
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) {
/** Compact canonical constructor: substitutes the defaults for any null component. */
public Settings {
if (colorMode == null) {
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) {
this(colorMode, glyphMode, MusicLevel.MEDIUM);
}
/**
* Settings used when nothing has been persisted yet.
*
* @return the default preferences
*/
public static Settings defaults() {
return new Settings(ConsoleIO.ColorMode.AUTO, ConsoleIO.GlyphMode.UNICODE, MusicLevel.MEDIUM);
}

View File

@@ -11,18 +11,31 @@ import java.nio.file.Path;
@Slf4j
public class SettingsStore {
/** Backing JSON file. */
private final Path file;
/** Jackson mapper used for both directions. */
private final ObjectMapper mapper = new ObjectMapper();
/** Uses the default {@code saves/settings.json} file. */
public SettingsStore() {
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) {
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() {
if (!Files.isRegularFile(file)) {
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) {
try {
if (file.getParent() != null) {