feat: condition-locked exits (+ room/dialogue state plumbing)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package thb.jeanluc.adventure.command.impl;
|
||||
|
||||
import thb.jeanluc.adventure.command.Command;
|
||||
import thb.jeanluc.adventure.game.Conditions;
|
||||
import thb.jeanluc.adventure.game.GameContext;
|
||||
import thb.jeanluc.adventure.model.Direction;
|
||||
import thb.jeanluc.adventure.model.ExitLock;
|
||||
import thb.jeanluc.adventure.model.Room;
|
||||
|
||||
import java.util.List;
|
||||
@@ -32,6 +34,11 @@ public class GoCommand implements Command {
|
||||
ctx.getIo().write("You can't go " + dir.getLabel() + " from here.");
|
||||
return;
|
||||
}
|
||||
ExitLock lock = ctx.getPlayer().getCurrentRoom().getExitLocks().get(dir);
|
||||
if (lock != null && !Conditions.all(lock.requires(), ctx)) {
|
||||
ctx.getIo().write(lock.blocked());
|
||||
return;
|
||||
}
|
||||
ctx.getPlayer().setCurrentRoom(next.get());
|
||||
new LookCommand().execute(ctx, List.of());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
package thb.jeanluc.adventure.loader;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import thb.jeanluc.adventure.loader.dto.ConditionDto;
|
||||
import thb.jeanluc.adventure.loader.dto.DescriptionStateDto;
|
||||
import thb.jeanluc.adventure.loader.dto.DialogueLineDto;
|
||||
import thb.jeanluc.adventure.loader.dto.EffectDto;
|
||||
import thb.jeanluc.adventure.loader.dto.ExitLockDto;
|
||||
import thb.jeanluc.adventure.loader.dto.NpcDto;
|
||||
import thb.jeanluc.adventure.loader.dto.ReactionDto;
|
||||
import thb.jeanluc.adventure.loader.dto.RoomDto;
|
||||
import thb.jeanluc.adventure.model.DescriptionState;
|
||||
import thb.jeanluc.adventure.model.DialogueLine;
|
||||
import thb.jeanluc.adventure.model.Direction;
|
||||
import thb.jeanluc.adventure.model.ExitLock;
|
||||
import thb.jeanluc.adventure.model.Npc;
|
||||
import thb.jeanluc.adventure.model.NpcReaction;
|
||||
import thb.jeanluc.adventure.model.Room;
|
||||
@@ -75,6 +83,25 @@ public class ReferenceResolver {
|
||||
room.addNpc(npc);
|
||||
}
|
||||
}
|
||||
if (dto.exitLocks() != null) {
|
||||
for (ExitLockDto lock : dto.exitLocks()) {
|
||||
Direction direction;
|
||||
try {
|
||||
direction = Direction.fromString(lock.direction());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new WorldLoadException(
|
||||
"Room '" + dto.id() + "' has exit lock with unknown direction '" + lock.direction() + "'");
|
||||
}
|
||||
room.addExitLock(direction, new ExitLock(
|
||||
ConditionDto.toModelList(lock.requires()), lock.blocked()));
|
||||
}
|
||||
}
|
||||
if (dto.descriptionStates() != null) {
|
||||
for (DescriptionStateDto ds : dto.descriptionStates()) {
|
||||
room.addDescriptionState(new DescriptionState(
|
||||
ConditionDto.toModelList(ds.when()), ds.text()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package thb.jeanluc.adventure.loader.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** YAML representation of a conditional room description. */
|
||||
public record DescriptionStateDto(List<ConditionDto> when, String text) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package thb.jeanluc.adventure.loader.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** YAML representation of a conditional NPC line. */
|
||||
public record DialogueLineDto(List<ConditionDto> when, String text) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package thb.jeanluc.adventure.loader.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** YAML representation of a single condition-gated exit. */
|
||||
public record ExitLockDto(String direction, List<ConditionDto> requires, String blocked) {
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import java.util.Map;
|
||||
* @param exits direction-name to target-room-id map
|
||||
* @param items ids of items initially in this room
|
||||
* @param npcs ids of NPCs initially in this room
|
||||
* @param exitLocks optional condition-gates per exit direction
|
||||
* @param descriptionStates optional condition-gated description variants
|
||||
*/
|
||||
public record RoomDto(
|
||||
String id,
|
||||
@@ -19,6 +21,13 @@ public record RoomDto(
|
||||
String description,
|
||||
Map<String, String> exits,
|
||||
List<String> items,
|
||||
List<String> npcs
|
||||
List<String> npcs,
|
||||
List<ExitLockDto> exitLocks,
|
||||
List<DescriptionStateDto> descriptionStates
|
||||
) {
|
||||
/** Backward-compatible constructor without the optional state fields. */
|
||||
public RoomDto(String id, String name, String description,
|
||||
Map<String, String> exits, List<String> items, List<String> npcs) {
|
||||
this(id, name, description, exits, items, npcs, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package thb.jeanluc.adventure.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** An alternate room description shown while its conditions hold. */
|
||||
public record DescriptionState(List<Condition> when, String text) {
|
||||
public DescriptionState {
|
||||
when = when == null ? List.of() : List.copyOf(when);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package thb.jeanluc.adventure.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** A conditional line an NPC says on {@code talk}, first match wins. */
|
||||
public record DialogueLine(List<Condition> when, String text) {
|
||||
public DialogueLine {
|
||||
when = when == null ? List.of() : List.copyOf(when);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package thb.jeanluc.adventure.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** A condition-gated exit with a message shown when it is blocked. */
|
||||
public record ExitLock(List<Condition> requires, String blocked) {
|
||||
public ExitLock {
|
||||
requires = requires == null ? List.of() : List.copyOf(requires);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import thb.jeanluc.adventure.model.item.Item;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -42,6 +44,12 @@ public class Room {
|
||||
/** NPCs currently in this room, keyed by npc id. Insertion-ordered for stable display. */
|
||||
private final LinkedHashMap<String, Npc> npcs = new LinkedHashMap<>();
|
||||
|
||||
/** Optional condition-gates per exit direction. */
|
||||
private final EnumMap<Direction, ExitLock> exitLocks = new EnumMap<>(Direction.class);
|
||||
|
||||
/** Optional condition-gated description variants, first match wins. */
|
||||
private final List<DescriptionState> descriptionStates = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Connects this room to another in the given direction. Does not
|
||||
* create the reverse connection — callers must set that up
|
||||
@@ -54,6 +62,25 @@ public class Room {
|
||||
exits.put(direction, neighbour);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a condition-gate to the exit in the given direction.
|
||||
*
|
||||
* @param direction the gated direction
|
||||
* @param lock the lock to apply
|
||||
*/
|
||||
public void addExitLock(Direction direction, ExitLock lock) {
|
||||
exitLocks.put(direction, lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a condition-gated description variant (first match wins).
|
||||
*
|
||||
* @param state the description variant
|
||||
*/
|
||||
public void addDescriptionState(DescriptionState state) {
|
||||
descriptionStates.add(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the room reachable in the given direction.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package thb.jeanluc.adventure.command.impl;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import thb.jeanluc.adventure.game.GameContext;
|
||||
import thb.jeanluc.adventure.io.TestIO;
|
||||
import thb.jeanluc.adventure.model.Condition;
|
||||
import thb.jeanluc.adventure.model.Direction;
|
||||
import thb.jeanluc.adventure.model.ExitLock;
|
||||
import thb.jeanluc.adventure.model.Player;
|
||||
import thb.jeanluc.adventure.model.Room;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LockedExitTest {
|
||||
@Test
|
||||
void lockedExitBlocksUntilFlagSet() {
|
||||
Room kitchen = new Room("kitchen", "Kitchen", "d");
|
||||
Room cellar = new Room("cellar", "Cellar", "d");
|
||||
kitchen.addExit(Direction.EAST, cellar);
|
||||
kitchen.addExitLock(Direction.EAST, new ExitLock(
|
||||
List.of(new Condition(Condition.Type.FLAG, "power_on")),
|
||||
"The cellar door is sealed."));
|
||||
Player player = new Player(kitchen, 0);
|
||||
TestIO io = new TestIO();
|
||||
GameContext ctx = new GameContext(null, player, io);
|
||||
|
||||
new GoCommand().execute(ctx, List.of("east"));
|
||||
assertThat(player.getCurrentRoom()).isEqualTo(kitchen);
|
||||
assertThat(io.allOutput()).contains("sealed");
|
||||
|
||||
ctx.getState().set("power_on");
|
||||
new GoCommand().execute(ctx, List.of("east"));
|
||||
assertThat(player.getCurrentRoom()).isEqualTo(cellar);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user