feat: conditional NPC dialogue

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 21:55:15 +02:00
parent c9c019cea1
commit 268c24d4e3
5 changed files with 76 additions and 3 deletions

View File

@@ -1,7 +1,9 @@
package thb.jeanluc.adventure.command.impl; package thb.jeanluc.adventure.command.impl;
import thb.jeanluc.adventure.command.Command; import thb.jeanluc.adventure.command.Command;
import thb.jeanluc.adventure.game.Conditions;
import thb.jeanluc.adventure.game.GameContext; import thb.jeanluc.adventure.game.GameContext;
import thb.jeanluc.adventure.model.DialogueLine;
import thb.jeanluc.adventure.model.Npc; import thb.jeanluc.adventure.model.Npc;
import java.util.List; import java.util.List;
@@ -24,7 +26,15 @@ public class TalkCommand implements Command {
ctx.getIo().write("There is no '" + npcId + "' here to talk to."); ctx.getIo().write("There is no '" + npcId + "' here to talk to.");
return; return;
} }
ctx.getIo().write(npc.get().getName() + " says: " + npc.get().getGreeting()); Npc found = npc.get();
String line = found.getGreeting();
for (DialogueLine dl : found.getDialogue()) {
if (Conditions.all(dl.when(), ctx)) {
line = dl.text();
break;
}
}
ctx.getIo().write(found.getName() + " says: " + line);
} }
@Override @Override

View File

@@ -113,10 +113,15 @@ public class ReferenceResolver {
*/ */
public void resolveNpcs(List<NpcDto> npcDtos) { public void resolveNpcs(List<NpcDto> npcDtos) {
for (NpcDto dto : npcDtos) { for (NpcDto dto : npcDtos) {
Npc npc = npcs.get(dto.id());
if (dto.dialogue() != null) {
for (DialogueLineDto dl : dto.dialogue()) {
npc.addDialogue(new DialogueLine(ConditionDto.toModelList(dl.when()), dl.text()));
}
}
if (dto.reactions() == null) { if (dto.reactions() == null) {
continue; continue;
} }
Npc npc = npcs.get(dto.id());
for (ReactionDto r : dto.reactions()) { for (ReactionDto r : dto.reactions()) {
if (r.onReceive() == null) { if (r.onReceive() == null) {
throw new WorldLoadException( throw new WorldLoadException(

View File

@@ -10,12 +10,18 @@ import java.util.List;
* @param description examine description * @param description examine description
* @param greeting text the NPC says on {@code talk} * @param greeting text the NPC says on {@code talk}
* @param reactions list of trigger/response definitions; may be null * @param reactions list of trigger/response definitions; may be null
* @param dialogue optional condition-gated dialogue lines; may be null
*/ */
public record NpcDto( public record NpcDto(
String id, String id,
String name, String name,
String description, String description,
String greeting, String greeting,
List<ReactionDto> reactions List<ReactionDto> reactions,
List<DialogueLineDto> dialogue
) { ) {
/** Backward-compatible constructor without dialogue. */
public NpcDto(String id, String name, String description, String greeting, List<ReactionDto> reactions) {
this(id, name, description, greeting, reactions, null);
}
} }

View File

@@ -3,7 +3,9 @@ package thb.jeanluc.adventure.model;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -34,6 +36,19 @@ public class Npc {
*/ */
private final Map<String, NpcReaction> reactions; private final Map<String, NpcReaction> reactions;
/** Optional condition-gated dialogue lines, first match wins; else {@link #greeting}. */
@Builder.Default
private final List<DialogueLine> dialogue = new ArrayList<>();
/**
* Adds a conditional dialogue line.
*
* @param line the dialogue line
*/
public void addDialogue(DialogueLine line) {
dialogue.add(line);
}
/** /**
* Looks up a reaction triggered by an item. * Looks up a reaction triggered by an item.
* *

View File

@@ -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.DialogueLine;
import thb.jeanluc.adventure.model.Npc;
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 DialogueTest {
@Test
void dialogueSwitchesWithFlagElseGreeting() {
Room room = new Room("k", "K", "d");
Npc man = Npc.shell("man", "Old Man", "stooped", "Hello there.");
man.addDialogue(new DialogueLine(
List.of(new Condition(Condition.Type.FLAG, "power_on")),
"The lights are back, bless you!"));
room.addNpc(man);
Player player = new Player(room, 0);
TestIO io = new TestIO();
GameContext ctx = new GameContext(null, player, io);
new TalkCommand().execute(ctx, List.of("man"));
assertThat(io.allOutput()).contains("Hello there");
io.outputs().clear();
ctx.getState().set("power_on");
new TalkCommand().execute(ctx, List.of("man"));
assertThat(io.allOutput()).contains("lights are back").doesNotContain("Hello there");
}
}