diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Light.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Light.java new file mode 100644 index 0000000..875ef55 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Light.java @@ -0,0 +1,44 @@ +package thb.jeanluc.adventure.game; + +import thb.jeanluc.adventure.model.Room; +import thb.jeanluc.adventure.model.item.Item; +import thb.jeanluc.adventure.model.item.SwitchableItem; + +import java.util.Collection; + +/** Decides whether a room is lit, given dark rooms and active light sources. */ +public final class Light { + + private Light() { + } + + /** A room is lit if it is not dark, or an active light source is carried or present. */ + public static boolean isLit(GameContext ctx, Room room) { + if (!room.isDark()) { + return true; + } + return hasActiveLight(ctx.getPlayer().getInventory().values()) + || hasActiveLight(room.getItems().values()); + } + + /** True if the player carries an active light source (for the HUD). */ + public static boolean carryingLight(GameContext ctx) { + return hasActiveLight(ctx.getPlayer().getInventory().values()); + } + + private static boolean hasActiveLight(Collection items) { + for (Item it : items) { + if (isActiveLight(it)) { + return true; + } + } + return false; + } + + private static boolean isActiveLight(Item it) { + if (!it.isLight()) { + return false; + } + return !(it instanceof SwitchableItem s) || s.isOn(); + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/ItemFactory.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/ItemFactory.java index 1bcefe0..a475c56 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/ItemFactory.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/ItemFactory.java @@ -30,12 +30,14 @@ public final class ItemFactory { .id(dto.id()) .name(dto.name()) .description(dto.description()) + .light(Boolean.TRUE.equals(dto.light())) .build(); case "readable" -> ReadableItem.builder() .id(dto.id()) .name(dto.name()) .description(dto.description()) .readText(dto.readText()) + .light(Boolean.TRUE.equals(dto.light())) .build(); case "switchable" -> SwitchableItem.builder() .id(dto.id()) @@ -45,6 +47,7 @@ public final class ItemFactory { .onText(dto.onText()) .offText(dto.offText()) .effects(thb.jeanluc.adventure.loader.dto.EffectDto.toModelList(dto.effects())) + .light(Boolean.TRUE.equals(dto.light())) .build(); default -> throw new WorldLoadException( "Unknown item type '" + dto.type() + "' on item '" + dto.id() + "'"); diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/RoomFactory.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/RoomFactory.java index caf93f4..5742eb9 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/RoomFactory.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/RoomFactory.java @@ -19,6 +19,8 @@ public final class RoomFactory { * @return the freshly built room shell */ public static Room shellFromDto(RoomDto dto) { - return new Room(dto.id(), dto.name(), dto.description()); + Room room = new Room(dto.id(), dto.name(), dto.description()); + room.setDark(Boolean.TRUE.equals(dto.dark())); + return room; } } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/ItemDto.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/ItemDto.java index 24ec7f1..3676590 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/ItemDto.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/ItemDto.java @@ -15,6 +15,7 @@ import java.util.List; * @param onText message printed when a switchable transitions to on * @param offText message printed when a switchable transitions to off * @param effects effects applied when a switchable transitions to on; nullable + * @param light whether this item is a light source; nullable */ public record ItemDto( String type, @@ -25,11 +26,12 @@ public record ItemDto( Boolean initialState, String onText, String offText, - List effects + List effects, + Boolean light ) { - /** Backward-compatible constructor without effects. */ + /** Backward-compatible constructor without effects/light. */ public ItemDto(String type, String id, String name, String description, String readText, Boolean initialState, String onText, String offText) { - this(type, id, name, description, readText, initialState, onText, offText, null); + this(type, id, name, description, readText, initialState, onText, offText, null, null); } } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/RoomDto.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/RoomDto.java index 9a42aca..e069351 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/RoomDto.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/RoomDto.java @@ -14,6 +14,7 @@ import java.util.Map; * @param npcs ids of NPCs initially in this room * @param exitLocks optional condition-gates per exit direction * @param descriptionStates optional condition-gated description variants + * @param dark whether this room is dark; nullable */ public record RoomDto( String id, @@ -23,11 +24,12 @@ public record RoomDto( List items, List npcs, List exitLocks, - List descriptionStates + List descriptionStates, + Boolean dark ) { /** Backward-compatible constructor without the optional state fields. */ public RoomDto(String id, String name, String description, Map exits, List items, List npcs) { - this(id, name, description, exits, items, npcs, null, null); + this(id, name, description, exits, items, npcs, null, null, null); } } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Room.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Room.java index eca90b2..dcd9e8c 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Room.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Room.java @@ -2,6 +2,7 @@ package thb.jeanluc.adventure.model; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import thb.jeanluc.adventure.model.item.Item; import java.util.ArrayList; @@ -50,6 +51,10 @@ public class Room { /** Optional condition-gated description variants, first match wins. */ private final List descriptionStates = new ArrayList<>(); + /** Whether this room is dark (needs a light source to enter/see). */ + @Setter + private boolean dark; + /** * Connects this room to another in the given direction. Does not * create the reverse connection — callers must set that up diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/Item.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/Item.java index 504d805..0058aaf 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/Item.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/Item.java @@ -33,6 +33,9 @@ public abstract class Item { /** Long description shown by the {@code examine} command. */ protected final String description; + /** Whether this item can serve as a light source (when on, for switchables). */ + protected final boolean light; + /** * Executes the item's primary action. Side effects (text output, * mutation of player state) flow through the given context. diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/LightTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/LightTest.java new file mode 100644 index 0000000..7ba72a9 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/LightTest.java @@ -0,0 +1,63 @@ +package thb.jeanluc.adventure.game; + +import org.junit.jupiter.api.Test; +import thb.jeanluc.adventure.io.TestIO; +import thb.jeanluc.adventure.model.Player; +import thb.jeanluc.adventure.model.Room; +import thb.jeanluc.adventure.model.item.PlainItem; +import thb.jeanluc.adventure.model.item.SwitchableItem; + +import static org.assertj.core.api.Assertions.assertThat; + +class LightTest { + private GameContext ctx(Player p) { + return new GameContext(null, p, new TestIO()); + } + + private SwitchableItem lamp(boolean on) { + return SwitchableItem.builder().id("lamp").name("Lamp").description("d") + .state(on).onText("on").offText("off").light(true).build(); + } + + @Test + void litRoomIsAlwaysLit() { + Room bright = new Room("r", "R", "d"); // dark defaults to false + assertThat(Light.isLit(ctx(new Player(bright, 0)), bright)).isTrue(); + } + + @Test + void darkRoomNeedsActiveLight() { + Room dark = new Room("r", "R", "d"); + dark.setDark(true); + Player p = new Player(dark, 0); + GameContext ctx = ctx(p); + assertThat(Light.isLit(ctx, dark)).isFalse(); + + p.addItem(lamp(false)); + assertThat(Light.isLit(ctx, dark)).isFalse(); // lamp off + + p.removeItem("lamp"); + p.addItem(lamp(true)); + assertThat(Light.isLit(ctx, dark)).isTrue(); // lamp on, carried + assertThat(Light.carryingLight(ctx)).isTrue(); + } + + @Test + void litLampLyingInRoomLightsIt() { + Room dark = new Room("r", "R", "d"); + dark.setDark(true); + dark.addItem(lamp(true)); + Player p = new Player(dark, 0); + assertThat(Light.isLit(ctx(p), dark)).isTrue(); + assertThat(Light.carryingLight(ctx(p))).isFalse(); // not carried + } + + @Test + void nonLightItemDoesNotLight() { + Room dark = new Room("r", "R", "d"); + dark.setDark(true); + Player p = new Player(dark, 0); + p.addItem(PlainItem.builder().id("rock").name("Rock").description("d").build()); + assertThat(Light.isLit(ctx(p), dark)).isFalse(); + } +}