From 258ab4f63bf033cb2a9e6bd42742683dbcfbdfd9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 31 May 2026 21:57:23 +0200 Subject: [PATCH] feat: NPC reactions gated by conditions, applying effects Co-Authored-By: Claude Opus 4.8 (1M context) --- .../adventure/command/impl/GiveCommand.java | 7 ++++ .../adventure/loader/ReferenceResolver.java | 2 + .../adventure/loader/dto/ReactionDto.java | 12 +++++- .../jeanluc/adventure/model/NpcReaction.java | 8 ++++ .../command/impl/ReactionEffectsTest.java | 42 +++++++++++++++++++ 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/ReactionEffectsTest.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/GiveCommand.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/GiveCommand.java index 8c3ac00..a48ca1b 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/GiveCommand.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/GiveCommand.java @@ -1,6 +1,8 @@ package thb.jeanluc.adventure.command.impl; import thb.jeanluc.adventure.command.Command; +import thb.jeanluc.adventure.game.Conditions; +import thb.jeanluc.adventure.game.Effects; import thb.jeanluc.adventure.game.GameContext; import thb.jeanluc.adventure.model.Npc; import thb.jeanluc.adventure.model.NpcReaction; @@ -44,12 +46,17 @@ public class GiveCommand implements Command { } NpcReaction reaction = reactionOpt.get(); + if (!Conditions.all(reaction.getRequires(), ctx)) { + ctx.getIo().write(npc.getName() + " isn't ready for that yet."); + return; + } if (reaction.getConsumes() != null) { ctx.getPlayer().removeItem(reaction.getConsumes().getId()); } if (reaction.getGives() != null) { ctx.getPlayer().addItem(reaction.getGives()); } + Effects.applyAll(reaction.getEffects(), ctx); ctx.getIo().write(npc.getName() + ": " + reaction.getResponse()); } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/ReferenceResolver.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/ReferenceResolver.java index 17eda2a..541877c 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/ReferenceResolver.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/ReferenceResolver.java @@ -155,6 +155,8 @@ public class ReferenceResolver { .consumes(consumes) .gives(gives) .response(r.response()) + .requires(ConditionDto.toModelList(r.requires())) + .effects(EffectDto.toModelList(r.effects())) .build(); npc.putReaction(r.onReceive(), reaction); } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/ReactionDto.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/ReactionDto.java index be1266a..2fb817f 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/ReactionDto.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/ReactionDto.java @@ -1,5 +1,7 @@ package thb.jeanluc.adventure.loader.dto; +import java.util.List; + /** * YAML representation of a single NPC reaction. * @@ -7,11 +9,19 @@ package thb.jeanluc.adventure.loader.dto; * @param response text the NPC says after the exchange * @param gives id of the item handed back; nullable * @param consumes id of the item taken from the player; nullable + * @param requires conditions that must hold; nullable + * @param effects effects applied after a successful exchange; nullable */ public record ReactionDto( String onReceive, String response, String gives, - String consumes + String consumes, + List requires, + List effects ) { + /** Backward-compatible constructor without requires/effects. */ + public ReactionDto(String onReceive, String response, String gives, String consumes) { + this(onReceive, response, gives, consumes, null, null); + } } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/NpcReaction.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/NpcReaction.java index 8751327..59afb55 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/NpcReaction.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/NpcReaction.java @@ -4,6 +4,8 @@ import lombok.Builder; import lombok.Getter; import thb.jeanluc.adventure.model.item.Item; +import java.util.List; + /** * A single trigger/response pair on an NPC. When the player gives the * NPC an item matching {@link #consumes}, the NPC writes {@link #response} @@ -21,4 +23,10 @@ public class NpcReaction { /** Text the NPC says after a successful exchange. */ private final String response; + + /** Conditions that must hold for the exchange; null/empty = always. */ + private final List requires; + + /** Effects applied after a successful exchange; null = none. */ + private final List effects; } diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/ReactionEffectsTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/ReactionEffectsTest.java new file mode 100644 index 0000000..2e99a17 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/ReactionEffectsTest.java @@ -0,0 +1,42 @@ +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.Effect; +import thb.jeanluc.adventure.model.Npc; +import thb.jeanluc.adventure.model.NpcReaction; +import thb.jeanluc.adventure.model.Player; +import thb.jeanluc.adventure.model.Room; +import thb.jeanluc.adventure.model.item.PlainItem; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ReactionEffectsTest { + @Test + void reactionRequiresFlagAndAppliesEffects() { + Room room = new Room("k", "K", "d"); + Npc man = Npc.shell("man", "Old Man", "stooped", "hi"); + man.putReaction("fuse", NpcReaction.builder() + .requires(List.of(new Condition(Condition.Type.FLAG, "cellar_drained"))) + .effects(List.of(new Effect(Effect.Type.SET_FLAG, "fuse_installed"))) + .response("Done.") + .build()); + room.addNpc(man); + Player player = new Player(room, 0); + player.addItem(PlainItem.builder().id("fuse").name("Fuse").description("d").build()); + TestIO io = new TestIO(); + GameContext ctx = new GameContext(null, player, io); + + new GiveCommand().execute(ctx, List.of("fuse", "man")); + assertThat(ctx.getState().isSet("fuse_installed")).isFalse(); // requires not met + + ctx.getState().set("cellar_drained"); + new GiveCommand().execute(ctx, List.of("fuse", "man")); + assertThat(ctx.getState().isSet("fuse_installed")).isTrue(); + assertThat(io.allOutput()).contains("Done."); + } +}