feat(menu): MainMenu, SettingsMenu, MenuAction (frontend-agnostic)

This commit is contained in:
2026-06-01 14:58:41 +02:00
parent d847928587
commit 687e88e20a
4 changed files with 171 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
package thb.jeanluc.adventure.menu;
import thb.jeanluc.adventure.io.GameIO;
import thb.jeanluc.adventure.save.SaveService;
import thb.jeanluc.adventure.save.SaveSlotInfo;
import java.util.ArrayList;
import java.util.List;
/** Frontend-agnostic main menu, slot picker, and new-game name prompt. */
public final class MainMenu {
private MainMenu() {
}
/** Shows the main menu and returns the chosen action. */
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}. */
public static String promptName(GameIO io, String defaultName) {
io.write("Name your save (Enter for \"" + defaultName + "\"):");
String line = io.readLine();
return (line == null || line.isBlank()) ? defaultName : line.trim();
}
/**
* 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".
*/
public static SaveSlotInfo pickSlot(GameIO io, SaveService saves) {
List<SaveSlotInfo> slots = saves.list();
if (slots.isEmpty()) {
io.write("No saved games.");
return null;
}
List<String> labels = new ArrayList<>();
for (SaveSlotInfo s : slots) {
labels.add(s.slotName() + " (" + s.roomId() + ", turn " + s.turn() + ")");
}
labels.add("Back");
int i = io.choose("LOAD GAME", labels);
return i < slots.size() ? slots.get(i) : null;
}
}

View File

@@ -0,0 +1,6 @@
package thb.jeanluc.adventure.menu;
/** Top-level main-menu choices, in display order. */
public enum MenuAction {
NEW_GAME, LOAD, SETTINGS, QUIT
}

View File

@@ -0,0 +1,67 @@
package thb.jeanluc.adventure.menu;
import thb.jeanluc.adventure.io.ConsoleIO;
import thb.jeanluc.adventure.io.GameIO;
import thb.jeanluc.adventure.save.Settings;
import thb.jeanluc.adventure.save.SettingsStore;
import java.util.List;
/** Minimal settings screen: toggle colour and glyph mode; persists + applies. */
public final class SettingsMenu {
private SettingsMenu() {
}
/**
* Loops the settings screen until the player picks "Back", persisting and
* applying each change. Returns the final settings.
*
* @param io active IO
* @param current starting settings
* @param store where to persist
* @param consoleIo the console IO to apply changes to live, or null if not console
*/
public static Settings show(GameIO io, Settings current, SettingsStore store, ConsoleIO consoleIo) {
Settings s = current;
while (true) {
int i = io.choose("SETTINGS", List.of(
"Colour: " + s.colorMode(),
"Glyphs: " + s.glyphMode(),
"Back"));
if (i == 0) {
s = new Settings(nextColor(s.colorMode()), s.glyphMode());
} else if (i == 1) {
s = new Settings(s.colorMode(), nextGlyph(s.glyphMode()));
} else {
return s;
}
store.save(s);
apply(s, consoleIo);
}
}
/** Applies colour/glyph settings to the console IO (no-op if null). */
public static void apply(Settings s, ConsoleIO consoleIo) {
if (consoleIo != null) {
consoleIo.setColorMode(s.colorMode());
consoleIo.setGlyphMode(s.glyphMode());
}
}
private static ConsoleIO.ColorMode nextColor(ConsoleIO.ColorMode m) {
return switch (m) {
case AUTO -> ConsoleIO.ColorMode.ON;
case ON -> ConsoleIO.ColorMode.OFF;
case OFF -> ConsoleIO.ColorMode.AUTO;
};
}
private static ConsoleIO.GlyphMode nextGlyph(ConsoleIO.GlyphMode m) {
return switch (m) {
case UNICODE -> ConsoleIO.GlyphMode.ASCII;
case ASCII -> ConsoleIO.GlyphMode.GLYPH;
case GLYPH -> ConsoleIO.GlyphMode.UNICODE;
};
}
}

View File

@@ -0,0 +1,49 @@
package thb.jeanluc.adventure.menu;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.save.SaveService;
import thb.jeanluc.adventure.save.Settings;
import thb.jeanluc.adventure.save.SettingsStore;
import thb.jeanluc.adventure.io.ConsoleIO;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class MenuTest {
@Test
void mainMenuMapsChoiceToAction() {
// options order: New Game(1) / Load(2) / Settings(3) / Quit(4)
assertThat(MainMenu.show(new TestIO().enqueue("1"))).isEqualTo(MenuAction.NEW_GAME);
assertThat(MainMenu.show(new TestIO().enqueue("2"))).isEqualTo(MenuAction.LOAD);
assertThat(MainMenu.show(new TestIO().enqueue("3"))).isEqualTo(MenuAction.SETTINGS);
assertThat(MainMenu.show(new TestIO().enqueue("4"))).isEqualTo(MenuAction.QUIT);
}
@Test
void promptNameUsesDefaultOnBlank() {
assertThat(MainMenu.promptName(new TestIO().enqueue(""), "Manor")).isEqualTo("Manor");
assertThat(MainMenu.promptName(new TestIO().enqueue("Spooky"), "Manor")).isEqualTo("Spooky");
}
@Test
void pickSlotReturnsNullWhenEmpty(@TempDir Path dir) {
TestIO io = new TestIO();
assertThat(MainMenu.pickSlot(io, new SaveService(dir))).isNull();
assertThat(io.allOutput()).contains("No saved games");
}
@Test
void settingsMenuTogglesGlyphAndPersists(@TempDir Path dir) {
SettingsStore store = new SettingsStore(dir.resolve("settings.json"));
// choose "Glyph mode" (assume option 2) → its toggle, then "Back" (last)
TestIO io = new TestIO().enqueue("2").enqueue("3");
Settings result = SettingsMenu.show(io, Settings.defaults(), store, new ConsoleIO());
// UNICODE default toggles to ASCII; persisted
assertThat(result.glyphMode()).isEqualTo(ConsoleIO.GlyphMode.ASCII);
assertThat(store.load().glyphMode()).isEqualTo(ConsoleIO.GlyphMode.ASCII);
}
}