feat: NPC reactions gated by conditions, applying effects

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

View File

@@ -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());
}

View File

@@ -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);
}

View File

@@ -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<ConditionDto> requires,
List<EffectDto> effects
) {
/** Backward-compatible constructor without requires/effects. */
public ReactionDto(String onReceive, String response, String gives, String consumes) {
this(onReceive, response, gives, consumes, null, null);
}
}

View File

@@ -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<Condition> requires;
/** Effects applied after a successful exchange; null = none. */
private final List<Effect> effects;
}

View File

@@ -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.");
}
}