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;
|
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.Effects;
|
||||||
import thb.jeanluc.adventure.game.GameContext;
|
import thb.jeanluc.adventure.game.GameContext;
|
||||||
import thb.jeanluc.adventure.model.Npc;
|
import thb.jeanluc.adventure.model.Npc;
|
||||||
import thb.jeanluc.adventure.model.NpcReaction;
|
import thb.jeanluc.adventure.model.NpcReaction;
|
||||||
@@ -44,12 +46,17 @@ public class GiveCommand implements Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NpcReaction reaction = reactionOpt.get();
|
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) {
|
if (reaction.getConsumes() != null) {
|
||||||
ctx.getPlayer().removeItem(reaction.getConsumes().getId());
|
ctx.getPlayer().removeItem(reaction.getConsumes().getId());
|
||||||
}
|
}
|
||||||
if (reaction.getGives() != null) {
|
if (reaction.getGives() != null) {
|
||||||
ctx.getPlayer().addItem(reaction.getGives());
|
ctx.getPlayer().addItem(reaction.getGives());
|
||||||
}
|
}
|
||||||
|
Effects.applyAll(reaction.getEffects(), ctx);
|
||||||
ctx.getIo().write(npc.getName() + ": " + reaction.getResponse());
|
ctx.getIo().write(npc.getName() + ": " + reaction.getResponse());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ public class ReferenceResolver {
|
|||||||
.consumes(consumes)
|
.consumes(consumes)
|
||||||
.gives(gives)
|
.gives(gives)
|
||||||
.response(r.response())
|
.response(r.response())
|
||||||
|
.requires(ConditionDto.toModelList(r.requires()))
|
||||||
|
.effects(EffectDto.toModelList(r.effects()))
|
||||||
.build();
|
.build();
|
||||||
npc.putReaction(r.onReceive(), reaction);
|
npc.putReaction(r.onReceive(), reaction);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package thb.jeanluc.adventure.loader.dto;
|
package thb.jeanluc.adventure.loader.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* YAML representation of a single NPC reaction.
|
* 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 response text the NPC says after the exchange
|
||||||
* @param gives id of the item handed back; nullable
|
* @param gives id of the item handed back; nullable
|
||||||
* @param consumes id of the item taken from the player; 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(
|
public record ReactionDto(
|
||||||
String onReceive,
|
String onReceive,
|
||||||
String response,
|
String response,
|
||||||
String gives,
|
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 lombok.Getter;
|
||||||
import thb.jeanluc.adventure.model.item.Item;
|
import thb.jeanluc.adventure.model.item.Item;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single trigger/response pair on an NPC. When the player gives the
|
* A single trigger/response pair on an NPC. When the player gives the
|
||||||
* NPC an item matching {@link #consumes}, the NPC writes {@link #response}
|
* 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. */
|
/** Text the NPC says after a successful exchange. */
|
||||||
private final String response;
|
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