fix(items): make scenery items fixed so they cannot be taken
Nothing stopped the player from picking up scenery. The front door is an ordinary switchable item in the foyer whose on-transition sets the 'left_manor' flag that every ending keys off, so it could be carried into the cellar and used there to end the game two floors underground. The chapel shrine could likewise be carried out and the locket/photograph ritual performed anywhere. Item gains a 'fixed' flag; TakeCommand refuses fixed items and leaves them in the room. Fixed items stay usable in place, so all recipes still work. Marked fixed: front_door, shrine, generator, valve, portrait — their descriptions already claimed they were bolted down.
This commit is contained in:
@@ -27,13 +27,19 @@ public class TakeCommand implements Command {
|
||||
}
|
||||
String itemId = args.getFirst();
|
||||
Room room = ctx.getPlayer().getCurrentRoom();
|
||||
Optional<Item> taken = room.removeItem(itemId);
|
||||
if (taken.isEmpty()) {
|
||||
Optional<Item> found = room.findItem(itemId);
|
||||
if (found.isEmpty()) {
|
||||
ctx.getIo().write("There is no '" + itemId + "' here.");
|
||||
return;
|
||||
}
|
||||
ctx.getPlayer().addItem(taken.get());
|
||||
ctx.getIo().write("You take the " + taken.get().getName() + ".");
|
||||
Item item = found.get();
|
||||
if (item.isFixed()) {
|
||||
ctx.getIo().write("The " + item.getName() + " is part of the room — you can't take it.");
|
||||
return;
|
||||
}
|
||||
room.removeItem(itemId);
|
||||
ctx.getPlayer().addItem(item);
|
||||
ctx.getIo().write("You take the " + item.getName() + ".");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,6 +13,7 @@ import thb.jeanluc.adventure.model.item.SwitchableItem;
|
||||
*/
|
||||
public final class ItemFactory {
|
||||
|
||||
/** Utility class; not instantiable. */
|
||||
private ItemFactory() {
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ public final class ItemFactory {
|
||||
.name(dto.name())
|
||||
.description(dto.description())
|
||||
.light(Boolean.TRUE.equals(dto.light()))
|
||||
.fixed(Boolean.TRUE.equals(dto.fixed()))
|
||||
.build();
|
||||
case "readable" -> ReadableItem.builder()
|
||||
.id(dto.id())
|
||||
@@ -38,6 +40,7 @@ public final class ItemFactory {
|
||||
.description(dto.description())
|
||||
.readText(dto.readText())
|
||||
.light(Boolean.TRUE.equals(dto.light()))
|
||||
.fixed(Boolean.TRUE.equals(dto.fixed()))
|
||||
.build();
|
||||
case "switchable" -> SwitchableItem.builder()
|
||||
.id(dto.id())
|
||||
@@ -48,6 +51,7 @@ public final class ItemFactory {
|
||||
.offText(dto.offText())
|
||||
.effects(thb.jeanluc.adventure.loader.dto.EffectDto.toModelList(dto.effects()))
|
||||
.light(Boolean.TRUE.equals(dto.light()))
|
||||
.fixed(Boolean.TRUE.equals(dto.fixed()))
|
||||
.build();
|
||||
default -> throw new WorldLoadException(
|
||||
"Unknown item type '" + dto.type() + "' on item '" + dto.id() + "'");
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.List;
|
||||
* @param offText message printed when a switchable transitions to off
|
||||
* @param effects effects applied when a switchable transitions to on; nullable
|
||||
* @param light whether this item is a light source; nullable
|
||||
* @param fixed whether this item is scenery that cannot be taken; nullable
|
||||
*/
|
||||
public record ItemDto(
|
||||
String type,
|
||||
@@ -27,11 +28,24 @@ public record ItemDto(
|
||||
String onText,
|
||||
String offText,
|
||||
List<EffectDto> effects,
|
||||
Boolean light
|
||||
Boolean light,
|
||||
Boolean fixed
|
||||
) {
|
||||
/** Backward-compatible constructor without effects/light. */
|
||||
/**
|
||||
* Backward-compatible constructor without effects/light/fixed, which default to null.
|
||||
*
|
||||
* @param type discriminator: {@code plain | readable | switchable}
|
||||
* @param id unique slug
|
||||
* @param name display name
|
||||
* @param description examine description
|
||||
* @param readText text printed when a readable item is used
|
||||
* @param initialState initial on/off state of a switchable item
|
||||
* @param onText message printed when a switchable transitions to on
|
||||
* @param offText message printed when a switchable transitions to off
|
||||
*/
|
||||
public ItemDto(String type, String id, String name, String description,
|
||||
String readText, Boolean initialState, String onText, String offText) {
|
||||
this(type, id, name, description, readText, initialState, onText, offText, null, null);
|
||||
this(type, id, name, description, readText, initialState, onText, offText,
|
||||
null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,13 @@ public abstract class Item {
|
||||
/** Whether this item can serve as a light source (when on, for switchables). */
|
||||
protected final boolean light;
|
||||
|
||||
/**
|
||||
* Whether this item is scenery bolted to its room and cannot be taken.
|
||||
* Fixed items (the front door, the chapel shrine, the boiler generator)
|
||||
* remain usable in place but must never enter the player's inventory.
|
||||
*/
|
||||
protected final boolean fixed;
|
||||
|
||||
/**
|
||||
* Executes the item's primary action. Side effects (text output,
|
||||
* mutation of player state) flow through the given context.
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
- type: plain
|
||||
id: valve
|
||||
name: Drainage Valve
|
||||
fixed: true
|
||||
description: |
|
||||
A rusted drainage valve set into the cellar wall. Its wheel is missing —
|
||||
it is part of the house, not something you can carry off.
|
||||
@@ -52,6 +53,7 @@
|
||||
- type: plain
|
||||
id: generator
|
||||
name: Generator
|
||||
fixed: true
|
||||
description: |
|
||||
A rusty generator with an empty fuse socket. It is bolted to the floor —
|
||||
you will have to work on it where it stands.
|
||||
@@ -71,6 +73,7 @@
|
||||
- type: plain
|
||||
id: shrine
|
||||
name: Stone Shrine
|
||||
fixed: true
|
||||
description: |
|
||||
A small stone shrine with a shallow offering niche. It is mortared into
|
||||
the chapel floor — a place to leave something, not to take it.
|
||||
@@ -80,6 +83,7 @@
|
||||
- type: switchable
|
||||
id: front_door
|
||||
name: Front Door
|
||||
fixed: true
|
||||
description: The heavy front door. It would let you leave the manor for good.
|
||||
initialState: false
|
||||
onText: |
|
||||
@@ -113,6 +117,7 @@
|
||||
- type: readable
|
||||
id: portrait
|
||||
name: Family Portrait
|
||||
fixed: true
|
||||
description: A faded oil portrait above the dining-room hearth.
|
||||
readText: |
|
||||
A stern family poses before the manor. A small brass plate reads only:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package thb.jeanluc.adventure.command.impl;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import thb.jeanluc.adventure.model.item.PlainItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Scenery items are usable in place but must never enter the inventory —
|
||||
* otherwise the front door could be carried off and opened from any room.
|
||||
*/
|
||||
class FixedItemTest {
|
||||
|
||||
@Test
|
||||
void take_withFixedItem_leavesItInTheRoom() {
|
||||
var w = new CommandTestSupport.World();
|
||||
var door = PlainItem.builder()
|
||||
.id("door").name("Front Door").description("heavy").fixed(true).build();
|
||||
w.kitchen.addItem(door);
|
||||
|
||||
new TakeCommand().execute(w.ctx, List.of("door"));
|
||||
|
||||
assertThat(w.player.hasItem("door")).isFalse();
|
||||
assertThat(w.kitchen.findItem("door")).isPresent();
|
||||
assertThat(w.io.allOutput()).contains("part of the room");
|
||||
}
|
||||
|
||||
@Test
|
||||
void take_withOrdinaryItem_stillMovesItToInventory() {
|
||||
var w = new CommandTestSupport.World();
|
||||
|
||||
new TakeCommand().execute(w.ctx, List.of("letter"));
|
||||
|
||||
assertThat(w.player.hasItem("letter")).isTrue();
|
||||
assertThat(w.kitchen.findItem("letter")).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user