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:
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user