feat: Ending model + endings.yaml loading (ordered, optional)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
package thb.jeanluc.adventure.loader;
|
||||||
|
|
||||||
|
import thb.jeanluc.adventure.loader.dto.ConditionDto;
|
||||||
|
import thb.jeanluc.adventure.loader.dto.EndingDto;
|
||||||
|
import thb.jeanluc.adventure.model.Ending;
|
||||||
|
|
||||||
|
/** Builds {@link Ending} objects from {@link EndingDto}s. */
|
||||||
|
public final class EndingFactory {
|
||||||
|
|
||||||
|
private EndingFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Ending fromDto(EndingDto dto) {
|
||||||
|
return new Ending(
|
||||||
|
dto.id(),
|
||||||
|
dto.title(),
|
||||||
|
Boolean.TRUE.equals(dto.victory()),
|
||||||
|
ConditionDto.toModelList(dto.when()),
|
||||||
|
dto.text());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,13 @@ import com.fasterxml.jackson.databind.type.CollectionType;
|
|||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import thb.jeanluc.adventure.loader.dto.EndingDto;
|
||||||
import thb.jeanluc.adventure.loader.dto.GameDto;
|
import thb.jeanluc.adventure.loader.dto.GameDto;
|
||||||
import thb.jeanluc.adventure.loader.dto.ItemDto;
|
import thb.jeanluc.adventure.loader.dto.ItemDto;
|
||||||
import thb.jeanluc.adventure.loader.dto.NpcDto;
|
import thb.jeanluc.adventure.loader.dto.NpcDto;
|
||||||
import thb.jeanluc.adventure.loader.dto.QuestDto;
|
import thb.jeanluc.adventure.loader.dto.QuestDto;
|
||||||
import thb.jeanluc.adventure.loader.dto.RoomDto;
|
import thb.jeanluc.adventure.loader.dto.RoomDto;
|
||||||
|
import thb.jeanluc.adventure.model.Ending;
|
||||||
import thb.jeanluc.adventure.model.Npc;
|
import thb.jeanluc.adventure.model.Npc;
|
||||||
import thb.jeanluc.adventure.model.Player;
|
import thb.jeanluc.adventure.model.Player;
|
||||||
import thb.jeanluc.adventure.model.Quest;
|
import thb.jeanluc.adventure.model.Quest;
|
||||||
@@ -19,6 +21,7 @@ import thb.jeanluc.adventure.model.item.Item;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -71,12 +74,14 @@ public class WorldLoader {
|
|||||||
List<NpcDto> npcDtos = readList(basePath + "/npcs.yaml", NpcDto.class);
|
List<NpcDto> npcDtos = readList(basePath + "/npcs.yaml", NpcDto.class);
|
||||||
List<RoomDto> roomDtos = readList(basePath + "/rooms.yaml", RoomDto.class);
|
List<RoomDto> roomDtos = readList(basePath + "/rooms.yaml", RoomDto.class);
|
||||||
List<QuestDto> questDtos = readListOptional(basePath + "/quests.yaml", QuestDto.class);
|
List<QuestDto> questDtos = readListOptional(basePath + "/quests.yaml", QuestDto.class);
|
||||||
|
List<EndingDto> endingDtos = readListOptional(basePath + "/endings.yaml", EndingDto.class);
|
||||||
GameDto gameDto = readSingle(basePath + "/game.yaml", GameDto.class);
|
GameDto gameDto = readSingle(basePath + "/game.yaml", GameDto.class);
|
||||||
|
|
||||||
requireUniqueIds("item", itemDtos.stream().map(ItemDto::id).toList());
|
requireUniqueIds("item", itemDtos.stream().map(ItemDto::id).toList());
|
||||||
requireUniqueIds("npc", npcDtos.stream().map(NpcDto::id).toList());
|
requireUniqueIds("npc", npcDtos.stream().map(NpcDto::id).toList());
|
||||||
requireUniqueIds("room", roomDtos.stream().map(RoomDto::id).toList());
|
requireUniqueIds("room", roomDtos.stream().map(RoomDto::id).toList());
|
||||||
requireUniqueIds("quest", questDtos.stream().map(QuestDto::id).toList());
|
requireUniqueIds("quest", questDtos.stream().map(QuestDto::id).toList());
|
||||||
|
requireUniqueIds("ending", endingDtos.stream().map(EndingDto::id).toList());
|
||||||
|
|
||||||
Map<String, Item> items = new HashMap<>();
|
Map<String, Item> items = new HashMap<>();
|
||||||
for (ItemDto dto : itemDtos) {
|
for (ItemDto dto : itemDtos) {
|
||||||
@@ -94,6 +99,10 @@ public class WorldLoader {
|
|||||||
for (QuestDto dto : questDtos) {
|
for (QuestDto dto : questDtos) {
|
||||||
quests.put(dto.id(), QuestFactory.fromDto(dto));
|
quests.put(dto.id(), QuestFactory.fromDto(dto));
|
||||||
}
|
}
|
||||||
|
List<Ending> endings = new ArrayList<>();
|
||||||
|
for (EndingDto dto : endingDtos) {
|
||||||
|
endings.add(EndingFactory.fromDto(dto));
|
||||||
|
}
|
||||||
|
|
||||||
ReferenceResolver resolver = new ReferenceResolver(items, npcs, rooms);
|
ReferenceResolver resolver = new ReferenceResolver(items, npcs, rooms);
|
||||||
resolver.resolveRooms(roomDtos);
|
resolver.resolveRooms(roomDtos);
|
||||||
@@ -106,7 +115,7 @@ public class WorldLoader {
|
|||||||
Player player = new Player(start, gold);
|
Player player = new Player(start, gold);
|
||||||
|
|
||||||
World world = new World(rooms, items, npcs,
|
World world = new World(rooms, items, npcs,
|
||||||
gameDto.title(), gameDto.welcomeMessage(), quests);
|
gameDto.title(), gameDto.welcomeMessage(), quests, endings);
|
||||||
log.info("World '{}' loaded: {} rooms, {} items, {} npcs",
|
log.info("World '{}' loaded: {} rooms, {} items, {} npcs",
|
||||||
gameDto.title(), rooms.size(), items.size(), npcs.size());
|
gameDto.title(), rooms.size(), items.size(), npcs.size());
|
||||||
return new LoadResult(world, player);
|
return new LoadResult(world, player);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package thb.jeanluc.adventure.loader.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** YAML representation of a game ending. */
|
||||||
|
public record EndingDto(String id, String title, Boolean victory, List<ConditionDto> when, String text) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package thb.jeanluc.adventure.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** A game ending: shown (and ends the game) when its conditions hold. */
|
||||||
|
public record Ending(String id, String title, boolean victory, List<Condition> when, String text) {
|
||||||
|
public Ending {
|
||||||
|
when = when == null ? List.of() : List.copyOf(when);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import lombok.Getter;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import thb.jeanluc.adventure.model.item.Item;
|
import thb.jeanluc.adventure.model.item.Item;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,9 +34,18 @@ public class World {
|
|||||||
/** Global lookup of quests by id. */
|
/** Global lookup of quests by id. */
|
||||||
private final Map<String, Quest> quests;
|
private final Map<String, Quest> quests;
|
||||||
|
|
||||||
/** Backward-compatible constructor for worlds without quests. */
|
/** Ordered list of endings (first matching one wins). */
|
||||||
|
private final List<Ending> endings;
|
||||||
|
|
||||||
|
/** Backward-compatible constructor for worlds without quests or endings. */
|
||||||
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());
|
this(rooms, items, npcs, title, welcomeMessage, Map.of(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Backward-compatible constructor for worlds with quests but no endings. */
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package thb.jeanluc.adventure.loader;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import thb.jeanluc.adventure.loader.dto.ConditionDto;
|
||||||
|
import thb.jeanluc.adventure.loader.dto.EndingDto;
|
||||||
|
import thb.jeanluc.adventure.model.Ending;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class EndingLoadingTest {
|
||||||
|
@Test
|
||||||
|
void factoryMapsFields() {
|
||||||
|
Ending e = EndingFactory.fromDto(new EndingDto("victory", "Won", true,
|
||||||
|
List.of(new ConditionDto("manor_secured", null, null)), "You win."));
|
||||||
|
assertThat(e.id()).isEqualTo("victory");
|
||||||
|
assertThat(e.victory()).isTrue();
|
||||||
|
assertThat(e.when()).hasSize(1);
|
||||||
|
assertThat(e.text()).isEqualTo("You win.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user