feat(loader): quests.yaml loading + startQuest effect mapping
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<QuestStage> 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()));
|
||||
}
|
||||
}
|
||||
@@ -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<ItemDto> itemDtos = readList(basePath + "/items.yaml", ItemDto.class);
|
||||
List<NpcDto> npcDtos = readList(basePath + "/npcs.yaml", NpcDto.class);
|
||||
List<RoomDto> roomDtos = readList(basePath + "/rooms.yaml", RoomDto.class);
|
||||
List<QuestDto> 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<String, Item> 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<String, Quest> 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 <T> List<T> readListOptional(String resource, Class<T> elementType) {
|
||||
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
||||
if (in == null) {
|
||||
return List.of();
|
||||
}
|
||||
CollectionType type = yaml.getTypeFactory().constructCollectionType(List.class, elementType);
|
||||
List<T> result = yaml.readValue(in, type);
|
||||
return result == null ? List.of() : result;
|
||||
} catch (IOException e) {
|
||||
throw new WorldLoadException("Failed to parse " + resource, e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T readSingle(String resource, Class<T> type) {
|
||||
try (InputStream in = openResource(resource)) {
|
||||
return yaml.readValue(in, type);
|
||||
|
||||
@@ -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<Effect> toModelList(List<EffectDto> dtos) {
|
||||
|
||||
@@ -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<QuestStageDto> stages, List<EffectDto> onComplete) {
|
||||
}
|
||||
@@ -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<ConditionDto> completion, List<EffectDto> onComplete) {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user