diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/QuestFactory.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/QuestFactory.java new file mode 100644 index 0000000..9f3e84f --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/QuestFactory.java @@ -0,0 +1,36 @@ +package thb.jeanluc.adventure.loader; + +import thb.jeanluc.adventure.loader.dto.ConditionDto; +import thb.jeanluc.adventure.loader.dto.EffectDto; +import thb.jeanluc.adventure.loader.dto.QuestDto; +import thb.jeanluc.adventure.loader.dto.QuestStageDto; +import thb.jeanluc.adventure.model.Quest; +import thb.jeanluc.adventure.model.QuestStage; + +import java.util.ArrayList; +import java.util.List; + +/** Builds {@link Quest} objects from {@link QuestDto}s (no reference resolution needed). */ +public final class QuestFactory { + + private QuestFactory() { + } + + public static Quest fromDto(QuestDto dto) { + List stages = new ArrayList<>(); + if (dto.stages() != null) { + for (QuestStageDto s : dto.stages()) { + stages.add(new QuestStage( + s.objective(), + ConditionDto.toModelList(s.completion()), + EffectDto.toModelList(s.onComplete()))); + } + } + return new Quest( + dto.id(), + dto.title(), + Boolean.TRUE.equals(dto.autoStart()), + stages, + EffectDto.toModelList(dto.onComplete())); + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/WorldLoader.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/WorldLoader.java index b6c8ee3..b783a63 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/WorldLoader.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/WorldLoader.java @@ -8,9 +8,11 @@ import lombok.extern.slf4j.Slf4j; import thb.jeanluc.adventure.loader.dto.GameDto; import thb.jeanluc.adventure.loader.dto.ItemDto; import thb.jeanluc.adventure.loader.dto.NpcDto; +import thb.jeanluc.adventure.loader.dto.QuestDto; import thb.jeanluc.adventure.loader.dto.RoomDto; import thb.jeanluc.adventure.model.Npc; import thb.jeanluc.adventure.model.Player; +import thb.jeanluc.adventure.model.Quest; import thb.jeanluc.adventure.model.Room; import thb.jeanluc.adventure.model.World; import thb.jeanluc.adventure.model.item.Item; @@ -68,11 +70,13 @@ public class WorldLoader { List itemDtos = readList(basePath + "/items.yaml", ItemDto.class); List npcDtos = readList(basePath + "/npcs.yaml", NpcDto.class); List roomDtos = readList(basePath + "/rooms.yaml", RoomDto.class); + List questDtos = readListOptional(basePath + "/quests.yaml", QuestDto.class); GameDto gameDto = readSingle(basePath + "/game.yaml", GameDto.class); requireUniqueIds("item", itemDtos.stream().map(ItemDto::id).toList()); requireUniqueIds("npc", npcDtos.stream().map(NpcDto::id).toList()); requireUniqueIds("room", roomDtos.stream().map(RoomDto::id).toList()); + requireUniqueIds("quest", questDtos.stream().map(QuestDto::id).toList()); Map items = new HashMap<>(); for (ItemDto dto : itemDtos) { @@ -86,6 +90,10 @@ public class WorldLoader { for (RoomDto dto : roomDtos) { rooms.put(dto.id(), RoomFactory.shellFromDto(dto)); } + Map quests = new HashMap<>(); + for (QuestDto dto : questDtos) { + quests.put(dto.id(), QuestFactory.fromDto(dto)); + } ReferenceResolver resolver = new ReferenceResolver(items, npcs, rooms); resolver.resolveRooms(roomDtos); @@ -98,7 +106,7 @@ public class WorldLoader { Player player = new Player(start, gold); World world = new World(rooms, items, npcs, - gameDto.title(), gameDto.welcomeMessage()); + gameDto.title(), gameDto.welcomeMessage(), quests); log.info("World '{}' loaded: {} rooms, {} items, {} npcs", gameDto.title(), rooms.size(), items.size(), npcs.size()); return new LoadResult(world, player); @@ -114,6 +122,19 @@ public class WorldLoader { } } + private List readListOptional(String resource, Class elementType) { + try (InputStream in = getClass().getResourceAsStream(resource)) { + if (in == null) { + return List.of(); + } + CollectionType type = yaml.getTypeFactory().constructCollectionType(List.class, elementType); + List result = yaml.readValue(in, type); + return result == null ? List.of() : result; + } catch (IOException e) { + throw new WorldLoadException("Failed to parse " + resource, e); + } + } + private T readSingle(String resource, Class type) { try (InputStream in = openResource(resource)) { return yaml.readValue(in, type); diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/EffectDto.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/EffectDto.java index ad57aba..25ae0aa 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/EffectDto.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/EffectDto.java @@ -7,7 +7,13 @@ import java.util.ArrayList; import java.util.List; /** YAML effect: exactly one of the fields is set. */ -public record EffectDto(String setFlag, String clearFlag, String giveItem, String removeItem, String say) { +public record EffectDto(String setFlag, String clearFlag, String giveItem, String removeItem, + String say, String startQuest) { + + /** Backward-compatible constructor without startQuest. */ + public EffectDto(String setFlag, String clearFlag, String giveItem, String removeItem, String say) { + this(setFlag, clearFlag, giveItem, removeItem, say, null); + } public Effect toModel() { if (setFlag != null) { @@ -25,7 +31,11 @@ public record EffectDto(String setFlag, String clearFlag, String giveItem, Strin if (say != null) { return new Effect(Effect.Type.SAY, say); } - throw new WorldLoadException("Effect must set one of setFlag/clearFlag/giveItem/removeItem/say"); + if (startQuest != null) { + return new Effect(Effect.Type.START_QUEST, startQuest); + } + throw new WorldLoadException( + "Effect must set one of setFlag/clearFlag/giveItem/removeItem/say/startQuest"); } public static List toModelList(List dtos) { diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/QuestDto.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/QuestDto.java new file mode 100644 index 0000000..0c16d15 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/QuestDto.java @@ -0,0 +1,8 @@ +package thb.jeanluc.adventure.loader.dto; + +import java.util.List; + +/** YAML representation of a quest. */ +public record QuestDto(String id, String title, Boolean autoStart, + List stages, List onComplete) { +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/QuestStageDto.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/QuestStageDto.java new file mode 100644 index 0000000..af8e698 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/QuestStageDto.java @@ -0,0 +1,7 @@ +package thb.jeanluc.adventure.loader.dto; + +import java.util.List; + +/** YAML representation of a quest stage. */ +public record QuestStageDto(String objective, List completion, List onComplete) { +} diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/QuestLoadingTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/QuestLoadingTest.java new file mode 100644 index 0000000..c762c92 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/QuestLoadingTest.java @@ -0,0 +1,38 @@ +package thb.jeanluc.adventure.loader; + +import org.junit.jupiter.api.Test; +import thb.jeanluc.adventure.loader.dto.ConditionDto; +import thb.jeanluc.adventure.loader.dto.EffectDto; +import thb.jeanluc.adventure.loader.dto.QuestDto; +import thb.jeanluc.adventure.loader.dto.QuestStageDto; +import thb.jeanluc.adventure.model.Effect; +import thb.jeanluc.adventure.model.Quest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class QuestLoadingTest { + @Test + void factoryMapsStagesConditionsAndEffects() { + QuestDto dto = new QuestDto("q", "Title", true, + List.of(new QuestStageDto("obj", + List.of(new ConditionDto("power_on", null, null)), + List.of(new EffectDto("done", null, null, null, null)))), + List.of()); + + Quest q = QuestFactory.fromDto(dto); + + assertThat(q.autoStart()).isTrue(); + assertThat(q.stages()).hasSize(1); + assertThat(q.stages().getFirst().objective()).isEqualTo("obj"); + assertThat(q.stages().getFirst().completion()).hasSize(1); + assertThat(q.stages().getFirst().onComplete().getFirst().type()).isEqualTo(Effect.Type.SET_FLAG); + } + + @Test + void startQuestEffectMaps() { + assertThat(new EffectDto(null, null, null, null, null, "q").toModel().type()) + .isEqualTo(Effect.Type.START_QUEST); + } +}