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();
|
String itemId = args.getFirst();
|
||||||
Room room = ctx.getPlayer().getCurrentRoom();
|
Room room = ctx.getPlayer().getCurrentRoom();
|
||||||
Optional<Item> taken = room.removeItem(itemId);
|
Optional<Item> found = room.findItem(itemId);
|
||||||
if (taken.isEmpty()) {
|
if (found.isEmpty()) {
|
||||||
ctx.getIo().write("There is no '" + itemId + "' here.");
|
ctx.getIo().write("There is no '" + itemId + "' here.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.getPlayer().addItem(taken.get());
|
Item item = found.get();
|
||||||
ctx.getIo().write("You take the " + taken.get().getName() + ".");
|
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
|
@Override
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import thb.jeanluc.adventure.model.item.SwitchableItem;
|
|||||||
*/
|
*/
|
||||||
public final class ItemFactory {
|
public final class ItemFactory {
|
||||||
|
|
||||||
|
/** Utility class; not instantiable. */
|
||||||
private ItemFactory() {
|
private ItemFactory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ public final class ItemFactory {
|
|||||||
.name(dto.name())
|
.name(dto.name())
|
||||||
.description(dto.description())
|
.description(dto.description())
|
||||||
.light(Boolean.TRUE.equals(dto.light()))
|
.light(Boolean.TRUE.equals(dto.light()))
|
||||||
|
.fixed(Boolean.TRUE.equals(dto.fixed()))
|
||||||
.build();
|
.build();
|
||||||
case "readable" -> ReadableItem.builder()
|
case "readable" -> ReadableItem.builder()
|
||||||
.id(dto.id())
|
.id(dto.id())
|
||||||
@@ -38,6 +40,7 @@ public final class ItemFactory {
|
|||||||
.description(dto.description())
|
.description(dto.description())
|
||||||
.readText(dto.readText())
|
.readText(dto.readText())
|
||||||
.light(Boolean.TRUE.equals(dto.light()))
|
.light(Boolean.TRUE.equals(dto.light()))
|
||||||
|
.fixed(Boolean.TRUE.equals(dto.fixed()))
|
||||||
.build();
|
.build();
|
||||||
case "switchable" -> SwitchableItem.builder()
|
case "switchable" -> SwitchableItem.builder()
|
||||||
.id(dto.id())
|
.id(dto.id())
|
||||||
@@ -48,6 +51,7 @@ public final class ItemFactory {
|
|||||||
.offText(dto.offText())
|
.offText(dto.offText())
|
||||||
.effects(thb.jeanluc.adventure.loader.dto.EffectDto.toModelList(dto.effects()))
|
.effects(thb.jeanluc.adventure.loader.dto.EffectDto.toModelList(dto.effects()))
|
||||||
.light(Boolean.TRUE.equals(dto.light()))
|
.light(Boolean.TRUE.equals(dto.light()))
|
||||||
|
.fixed(Boolean.TRUE.equals(dto.fixed()))
|
||||||
.build();
|
.build();
|
||||||
default -> throw new WorldLoadException(
|
default -> throw new WorldLoadException(
|
||||||
"Unknown item type '" + dto.type() + "' on item '" + dto.id() + "'");
|
"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 offText message printed when a switchable transitions to off
|
||||||
* @param effects effects applied when a switchable transitions to on; nullable
|
* @param effects effects applied when a switchable transitions to on; nullable
|
||||||
* @param light whether this item is a light source; 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(
|
public record ItemDto(
|
||||||
String type,
|
String type,
|
||||||
@@ -27,11 +28,24 @@ public record ItemDto(
|
|||||||
String onText,
|
String onText,
|
||||||
String offText,
|
String offText,
|
||||||
List<EffectDto> effects,
|
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,
|
public ItemDto(String type, String id, String name, String description,
|
||||||
String readText, Boolean initialState, String onText, String offText) {
|
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). */
|
/** Whether this item can serve as a light source (when on, for switchables). */
|
||||||
protected final boolean light;
|
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,
|
* Executes the item's primary action. Side effects (text output,
|
||||||
* mutation of player state) flow through the given context.
|
* mutation of player state) flow through the given context.
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
- type: plain
|
- type: plain
|
||||||
id: valve
|
id: valve
|
||||||
name: Drainage Valve
|
name: Drainage Valve
|
||||||
|
fixed: true
|
||||||
description: |
|
description: |
|
||||||
A rusted drainage valve set into the cellar wall. Its wheel is missing —
|
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.
|
it is part of the house, not something you can carry off.
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
- type: plain
|
- type: plain
|
||||||
id: generator
|
id: generator
|
||||||
name: Generator
|
name: Generator
|
||||||
|
fixed: true
|
||||||
description: |
|
description: |
|
||||||
A rusty generator with an empty fuse socket. It is bolted to the floor —
|
A rusty generator with an empty fuse socket. It is bolted to the floor —
|
||||||
you will have to work on it where it stands.
|
you will have to work on it where it stands.
|
||||||
@@ -71,6 +73,7 @@
|
|||||||
- type: plain
|
- type: plain
|
||||||
id: shrine
|
id: shrine
|
||||||
name: Stone Shrine
|
name: Stone Shrine
|
||||||
|
fixed: true
|
||||||
description: |
|
description: |
|
||||||
A small stone shrine with a shallow offering niche. It is mortared into
|
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.
|
the chapel floor — a place to leave something, not to take it.
|
||||||
@@ -80,6 +83,7 @@
|
|||||||
- type: switchable
|
- type: switchable
|
||||||
id: front_door
|
id: front_door
|
||||||
name: Front Door
|
name: Front Door
|
||||||
|
fixed: true
|
||||||
description: The heavy front door. It would let you leave the manor for good.
|
description: The heavy front door. It would let you leave the manor for good.
|
||||||
initialState: false
|
initialState: false
|
||||||
onText: |
|
onText: |
|
||||||
@@ -113,6 +117,7 @@
|
|||||||
- type: readable
|
- type: readable
|
||||||
id: portrait
|
id: portrait
|
||||||
name: Family Portrait
|
name: Family Portrait
|
||||||
|
fixed: true
|
||||||
description: A faded oil portrait above the dining-room hearth.
|
description: A faded oil portrait above the dining-room hearth.
|
||||||
readText: |
|
readText: |
|
||||||
A stern family poses before the manor. A small brass plate reads only:
|
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