Compare commits
2 Commits
f8f7eb90f0
...
8e63f21c7d
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e63f21c7d | |||
| 41c79c3b96 |
@@ -26,14 +26,17 @@ werden aus YAML-Dateien geladen, nicht im Code festverdrahtet.
|
||||
|
||||
## Schnellstart ohne Maven
|
||||
|
||||
Im Ordner [`release/`](release/) liegt eine fertig gebaute, eigenständige JAR
|
||||
(`HauntedManor.jar`) mit allen Abhängigkeiten. Sie braucht nur Java 25:
|
||||
Im Ordner [`release/`](release/) liegen zwei fertig gebaute, eigenständige
|
||||
JARs mit allen Abhängigkeiten – je eine pro Frontend. Sie brauchen nur Java 25:
|
||||
|
||||
```bash
|
||||
java -jar release/HauntedManor.jar # Konsolen-Version
|
||||
java -cp release/HauntedManor.jar thb.jeanluc.adventure.AppGui # Swing-GUI
|
||||
java -jar release/HauntedManor-Console.jar # Konsolen-Version (~5 MB, ohne Audio)
|
||||
java -jar release/HauntedManor-GUI.jar # Swing-GUI (~17 MB, mit Musik)
|
||||
```
|
||||
|
||||
Die Konsolen-JAR enthält weder die Musikdateien noch die OGG-Bibliotheken –
|
||||
der Textmodus spielt keine Musik, daher werden sie dort weggelassen.
|
||||
|
||||
## Bauen und Testen
|
||||
|
||||
Mit dem Wrapper (empfohlen, keine Maven-Installation nötig) – unter Windows
|
||||
@@ -41,7 +44,7 @@ Mit dem Wrapper (empfohlen, keine Maven-Installation nötig) – unter Windows
|
||||
|
||||
```bash
|
||||
./mvnw clean test # alle Tests ausführen (233 Tests)
|
||||
./mvnw clean package # baut die eigenständige JAR nach release/HauntedManor.jar
|
||||
./mvnw clean package # baut beide JARs nach release/ (Console + GUI)
|
||||
```
|
||||
|
||||
Ein systemweit installiertes `mvn` funktioniert identisch (`mvn clean test` …).
|
||||
|
||||
@@ -113,25 +113,65 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<!-- Two self-contained jars in release/: one per entry point.
|
||||
createDependencyReducedPom is off globally so no stray pom is written. -->
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
<executions>
|
||||
<!-- Console version -> release/HauntedManor-Console.jar -->
|
||||
<execution>
|
||||
<id>shade-console</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<!-- Self-contained, runnable JAR with all dependencies bundled. -->
|
||||
<outputFile>${project.basedir}/release/HauntedManor.jar</outputFile>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<outputFile>${project.basedir}/release/HauntedManor-Console.jar</outputFile>
|
||||
<!-- Console has no audio (GameIO.setMusic is a no-op there), so leave
|
||||
out the OGG decode libraries and the ~11 MB of music tracks. -->
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>com.googlecode.soundlibs:vorbisspi</exclude>
|
||||
<exclude>com.googlecode.soundlibs:jorbis</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>thb.jeanluc:Semesterprojekt</artifact>
|
||||
<excludes>
|
||||
<exclude>music/**</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<transformers>
|
||||
<!-- Default entry point: console version. GUI via -cp ... AppGui. -->
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>thb.jeanluc.adventure.App</Main-Class>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
<!-- Merge META-INF/services so the OGG Vorbis SPI keeps working. -->
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
<!-- Swing GUI version -> release/HauntedManor-GUI.jar -->
|
||||
<execution>
|
||||
<id>shade-gui</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputFile>${project.basedir}/release/HauntedManor-GUI.jar</outputFile>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>thb.jeanluc.adventure.AppGui</Main-Class>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
</transformers>
|
||||
|
||||
BIN
Semesterprojekt/release/HauntedManor-Console.jar
Normal file
BIN
Semesterprojekt/release/HauntedManor-Console.jar
Normal file
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ package thb.jeanluc.adventure.command.impl;
|
||||
|
||||
import thb.jeanluc.adventure.command.Command;
|
||||
import thb.jeanluc.adventure.game.Conditions;
|
||||
import thb.jeanluc.adventure.game.EscortEngine;
|
||||
import thb.jeanluc.adventure.game.GameContext;
|
||||
import thb.jeanluc.adventure.game.Light;
|
||||
import thb.jeanluc.adventure.io.text.RoomView;
|
||||
@@ -36,6 +37,10 @@ public class LookCommand implements Command {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String followers = EscortEngine.followerNote(ctx);
|
||||
if (followers != null) {
|
||||
description = description.stripTrailing() + "\n\n" + followers;
|
||||
}
|
||||
ctx.getIo().showRoom(new RoomView(room.getName(), description, items, npcs, exits));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package thb.jeanluc.adventure.game;
|
||||
|
||||
import thb.jeanluc.adventure.io.text.StyledText;
|
||||
import thb.jeanluc.adventure.model.Npc;
|
||||
import thb.jeanluc.adventure.model.Room;
|
||||
import thb.jeanluc.adventure.model.item.Item;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Drives the optional "Lost Twins" escort side-quest. Once a twin is recruited
|
||||
* (a {@code give} reaction sets its {@code *_following} flag) the twin tags
|
||||
* along with the player: this engine removes them from their home room and
|
||||
* renders them as a follower. When the player reaches the foyer with both
|
||||
* twins in tow, the brothers are reunited — granting the reward and firing the
|
||||
* {@code twins_reunited} flag that unlocks the bonus ending.
|
||||
*
|
||||
* <p>State lives entirely in flags, so a save/load round-trip needs no extra
|
||||
* persistence: {@link #tick} reconciles follower placement from the restored
|
||||
* flags on the first turn after loading.
|
||||
*/
|
||||
public final class EscortEngine {
|
||||
|
||||
/** Room the twins must be led to in order to reunite and escape. */
|
||||
static final String REUNION_ROOM_ID = "foyer";
|
||||
|
||||
/** Flag set (and bonus-ending gate) once the brothers are reunited. */
|
||||
static final String REUNITED_FLAG = "twins_reunited";
|
||||
|
||||
/** Item handed to the player as thanks; pure flavour, no mechanical effect. */
|
||||
static final String REWARD_ITEM_ID = "brass_compass";
|
||||
|
||||
/** The two escortable twins, each pinned to a home room and a follow flag. */
|
||||
private static final List<Twin> TWINS = List.of(
|
||||
new Twin("marlon", "marlon_following", "conservatory"),
|
||||
new Twin("malte", "malte_following", "boiler_room"));
|
||||
|
||||
private EscortEngine() {
|
||||
}
|
||||
|
||||
/** A recruitable twin and the state that tracks their escort. */
|
||||
private record Twin(String npcId, String followingFlag, String homeRoomId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-turn escort progression: pull recruited twins out of their home rooms
|
||||
* and, when both stand with the player in the foyer, reunite them.
|
||||
*
|
||||
* @param ctx the active game context
|
||||
*/
|
||||
public static void tick(GameContext ctx) {
|
||||
for (Twin twin : TWINS) {
|
||||
if (isFollowing(ctx, twin)) {
|
||||
Room home = ctx.getWorld().getRooms().get(twin.homeRoomId());
|
||||
if (home != null) {
|
||||
home.removeNpc(twin.npcId());
|
||||
}
|
||||
}
|
||||
}
|
||||
maybeReunite(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* A short line naming the twins currently at the player's heels, or
|
||||
* {@code null} when nobody is following. Appended to room descriptions.
|
||||
*
|
||||
* @param ctx the active game context
|
||||
* @return the follower note, or {@code null}
|
||||
*/
|
||||
public static String followerNote(GameContext ctx) {
|
||||
if (ctx.getState().isSet(REUNITED_FLAG)) {
|
||||
return null;
|
||||
}
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Twin twin : TWINS) {
|
||||
if (isFollowing(ctx, twin)) {
|
||||
names.add(displayName(ctx, twin));
|
||||
}
|
||||
}
|
||||
if (names.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (names.size() == 1) {
|
||||
return names.getFirst() + " keeps close at your shoulder.";
|
||||
}
|
||||
return String.join(" and ", names) + " keep close at your shoulders.";
|
||||
}
|
||||
|
||||
private static void maybeReunite(GameContext ctx) {
|
||||
if (ctx.getState().isSet(REUNITED_FLAG)) {
|
||||
return;
|
||||
}
|
||||
boolean inFoyer = REUNION_ROOM_ID.equals(ctx.getPlayer().getCurrentRoom().getId());
|
||||
boolean bothFollowing = TWINS.stream().allMatch(t -> isFollowing(ctx, t));
|
||||
if (!inFoyer || !bothFollowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.getState().set(REUNITED_FLAG);
|
||||
grantReward(ctx);
|
||||
ctx.getIo().print(StyledText.builder()
|
||||
.heading("Marlon and Malte are reunited! ")
|
||||
.plain("The brothers crash together in the lamplight, clinging on as if "
|
||||
+ "the dark might split them again. Then they're gone — out the "
|
||||
+ "front door and into the night, hand in hand, free.")
|
||||
.build());
|
||||
}
|
||||
|
||||
private static void grantReward(GameContext ctx) {
|
||||
Item reward = ctx.getWorld().getItems().get(REWARD_ITEM_ID);
|
||||
if (reward != null && !ctx.getPlayer().hasItem(REWARD_ITEM_ID)) {
|
||||
ctx.getPlayer().addItem(reward);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isFollowing(GameContext ctx, Twin twin) {
|
||||
return ctx.getState().isSet(twin.followingFlag());
|
||||
}
|
||||
|
||||
private static String displayName(GameContext ctx, Twin twin) {
|
||||
Npc npc = ctx.getWorld().getNpcs().get(twin.npcId());
|
||||
return npc != null ? npc.getName() : twin.npcId();
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ public class Game {
|
||||
|
||||
private void publishHud() {
|
||||
QuestEngine.tick(ctx);
|
||||
EscortEngine.tick(ctx);
|
||||
ctx.getIo().setHud(new Hud(
|
||||
ctx.getPlayer().getCurrentRoom().getName(),
|
||||
ctx.getPlayer().getGold(),
|
||||
|
||||
@@ -147,4 +147,14 @@ public class Room {
|
||||
public Optional<Npc> findNpc(String npcId) {
|
||||
return Optional.ofNullable(npcs.get(npcId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an NPC from this room by id.
|
||||
*
|
||||
* @param npcId id of the NPC to remove
|
||||
* @return the removed NPC, or empty if no such NPC was present
|
||||
*/
|
||||
public Optional<Npc> removeNpc(String npcId) {
|
||||
return Optional.ofNullable(npcs.remove(npcId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
# specific (best) ending is listed first. ghost_banished implies power_on
|
||||
# implies the upper floor was reached, so #1 needs only left_manor + ghost_banished.
|
||||
|
||||
# Best possible end: spirit laid to rest AND the lost twins led out (optional).
|
||||
- id: no_one_left_behind
|
||||
title: "No One Left Behind"
|
||||
victory: true
|
||||
when: [{ flag: left_manor }, { flag: ghost_banished }, { flag: twins_reunited }]
|
||||
text: |
|
||||
You step into the night, and you are not alone. Marlon and Malte wait at
|
||||
the gate, together, breathing the cold free air. Behind you the manor is
|
||||
utterly still. You laid a spirit to rest and brought two brothers home —
|
||||
there is no better way out of that house than this one.
|
||||
|
||||
- id: spirit_at_rest
|
||||
title: "Spirit at Rest"
|
||||
victory: true
|
||||
|
||||
@@ -122,3 +122,27 @@
|
||||
id: shovel
|
||||
name: Shovel
|
||||
description: A rusty shovel. Sturdy enough to dig, though nothing here needs digging.
|
||||
|
||||
# ── The lost twins (optional escort side-quest) ──────────────────────────────
|
||||
|
||||
- type: plain
|
||||
id: marlon_keepsake
|
||||
name: Blue Marble
|
||||
description: |
|
||||
A child's blue glass marble, worn smooth from years in a pocket. Marlon's.
|
||||
Somewhere its red twin is being clutched just as tightly.
|
||||
|
||||
- type: plain
|
||||
id: malte_keepsake
|
||||
name: Red Marble
|
||||
description: |
|
||||
A child's red glass marble, the mate to a blue one. Malte's — the only
|
||||
thing he kept hold of when the dark took him.
|
||||
|
||||
- type: readable
|
||||
id: brass_compass
|
||||
name: Brass Compass
|
||||
description: A small brass compass, pressed on you by two very grateful brothers.
|
||||
readText: |
|
||||
Engraved on the back, in a child's scratchy hand: "For the one who led us
|
||||
out. — M & M." The needle, you notice, always settles toward the door.
|
||||
|
||||
@@ -27,3 +27,54 @@
|
||||
consumes: letter
|
||||
effects:
|
||||
- { setFlag: heard_tale }
|
||||
|
||||
# ── The lost twins (optional escort side-quest) ──────────────────────────────
|
||||
# Marlon (conservatory) and Malte (boiler room) were separated in the manor.
|
||||
# Give each the other's marble as proof his brother lives, and he follows you;
|
||||
# lead both to the foyer and they escape together. See EscortEngine.
|
||||
|
||||
- id: marlon
|
||||
name: Marlon
|
||||
description: |
|
||||
A frightened young man pressed into the corner, knuckles white around a
|
||||
blue glass marble. He has his brother's face — minus the brother.
|
||||
greeting: |
|
||||
"Please — have you seen Malte? My twin. We were exploring and the dark
|
||||
just... took him. I can't leave this house without him. I can't."
|
||||
dialogue:
|
||||
- when: [{ hasItem: malte_keepsake }]
|
||||
text: |
|
||||
"That red marble — that's MALTE'S! He's alive? Here, quick: give it to
|
||||
me so I know it's real. Then I'll follow you to him."
|
||||
reactions:
|
||||
- onReceive: malte_keepsake
|
||||
response: |
|
||||
"His marble. His actual marble. Then he's alive — and you've seen him.
|
||||
Lead the way. I'm right behind you, I won't lose him again."
|
||||
consumes: malte_keepsake
|
||||
effects:
|
||||
- { setFlag: marlon_following }
|
||||
- { startQuest: reunite_twins }
|
||||
|
||||
- id: malte
|
||||
name: Malte
|
||||
description: |
|
||||
A shivering young man clutching a red glass marble in the furnace-dark.
|
||||
The image of his brother, alone.
|
||||
greeting: |
|
||||
"Who's there? Marlon? No... Please, have you seen him — my twin, Marlon?
|
||||
We got split up and I can't find my way back in this dark."
|
||||
dialogue:
|
||||
- when: [{ hasItem: marlon_keepsake }]
|
||||
text: |
|
||||
"A blue marble — Marlon's! You've found him! Give it here so I know
|
||||
it's real, then take me to him, please, before I lose my nerve."
|
||||
reactions:
|
||||
- onReceive: marlon_keepsake
|
||||
response: |
|
||||
"Marlon's marble. He's really here. Don't let go of me — wherever
|
||||
you're going, I'm following. Take me to my brother."
|
||||
consumes: marlon_keepsake
|
||||
effects:
|
||||
- { setFlag: malte_following }
|
||||
- { startQuest: reunite_twins }
|
||||
|
||||
@@ -16,3 +16,13 @@
|
||||
stages:
|
||||
- objective: "Lay the restless spirit to rest in the chapel."
|
||||
completion: [{ flag: ghost_banished }]
|
||||
|
||||
# Optional side-quest: started when you first recruit a twin (see npcs.yaml).
|
||||
- id: reunite_twins
|
||||
title: "The Lost Twins"
|
||||
autoStart: false
|
||||
stages:
|
||||
- objective: "Reunite Marlon and Malte — lead them both to the entrance."
|
||||
completion: [{ flag: twins_reunited }]
|
||||
onComplete:
|
||||
- { say: "The brothers are together again, and gone into the night. You didn't have to help them. You're glad you did." }
|
||||
|
||||
@@ -74,11 +74,18 @@
|
||||
description: |
|
||||
A glass-roofed room choked with dead ferns. Rusted plumbing lines the
|
||||
walls, and a heavy iron valve wheel has been left on a potting bench.
|
||||
A frightened young man cowers among the planters, a blue marble in his fist.
|
||||
music: manor-theme.ogg
|
||||
exits:
|
||||
south: dining_room
|
||||
items: [valve_wheel]
|
||||
npcs: []
|
||||
items: [valve_wheel, marlon_keepsake]
|
||||
npcs: [marlon]
|
||||
descriptionStates:
|
||||
- when: [{ flag: marlon_following }]
|
||||
text: |
|
||||
A glass-roofed room choked with dead ferns. Rusted plumbing lines the
|
||||
walls, and a heavy iron valve wheel has been left on a potting bench.
|
||||
The corner where the frightened young man crouched is empty now.
|
||||
|
||||
- id: grand_staircase
|
||||
name: Grand Staircase
|
||||
@@ -135,13 +142,20 @@
|
||||
name: Boiler Room
|
||||
description: |
|
||||
A cramped stone room behind the furnace. A rusty generator squats in the
|
||||
corner, an empty fuse socket gaping in its housing.
|
||||
corner, an empty fuse socket gaping in its housing. A shivering young man
|
||||
huddles against the cold iron, a red marble clenched in his hand.
|
||||
music: cellar-drone.ogg
|
||||
dark: true
|
||||
exits:
|
||||
east: cellar_stairs
|
||||
items: [generator]
|
||||
npcs: []
|
||||
items: [generator, malte_keepsake]
|
||||
npcs: [malte]
|
||||
descriptionStates:
|
||||
- when: [{ flag: malte_following }]
|
||||
text: |
|
||||
A cramped stone room behind the furnace. A rusty generator squats in
|
||||
the corner, an empty fuse socket gaping in its housing. The cold spot
|
||||
by the iron where the shivering man huddled is empty now.
|
||||
|
||||
# ── Upper floor (reachable only once the power is on) ───────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package thb.jeanluc.adventure.game;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import thb.jeanluc.adventure.io.TestIO;
|
||||
import thb.jeanluc.adventure.model.Npc;
|
||||
import thb.jeanluc.adventure.model.Player;
|
||||
import thb.jeanluc.adventure.model.Room;
|
||||
import thb.jeanluc.adventure.model.World;
|
||||
import thb.jeanluc.adventure.model.item.Item;
|
||||
import thb.jeanluc.adventure.model.item.PlainItem;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class EscortEngineTest {
|
||||
|
||||
private final Room foyer = new Room("foyer", "Foyer", "An empty foyer.");
|
||||
private final Room conservatory = new Room("conservatory", "Conservatory", "Ferns.");
|
||||
private final Room boilerRoom = new Room("boiler_room", "Boiler Room", "Furnace-dark.");
|
||||
private final Npc marlon = Npc.shell("marlon", "Marlon", "A twin.", "...");
|
||||
private final Npc malte = Npc.shell("malte", "Malte", "A twin.", "...");
|
||||
|
||||
private GameContext ctx(Room start) {
|
||||
conservatory.addNpc(marlon);
|
||||
boilerRoom.addNpc(malte);
|
||||
Item compass = PlainItem.builder()
|
||||
.id("brass_compass").name("Brass Compass").description("d").build();
|
||||
World w = new World(
|
||||
Map.of("foyer", foyer, "conservatory", conservatory, "boiler_room", boilerRoom),
|
||||
Map.of("brass_compass", compass),
|
||||
Map.of("marlon", marlon, "malte", malte),
|
||||
"t", "w");
|
||||
return new GameContext(w, new Player(start, 0), new TestIO());
|
||||
}
|
||||
|
||||
@Test
|
||||
void recruitedTwinIsPulledFromHomeRoomAndAppearsInFollowerNote() {
|
||||
GameContext ctx = ctx(conservatory);
|
||||
ctx.getState().set("marlon_following");
|
||||
|
||||
EscortEngine.tick(ctx);
|
||||
|
||||
assertThat(conservatory.findNpc("marlon")).isEmpty();
|
||||
assertThat(EscortEngine.followerNote(ctx)).contains("Marlon").contains("shoulder");
|
||||
}
|
||||
|
||||
@Test
|
||||
void followerNoteListsBothTwinsWhenBothFollow() {
|
||||
GameContext ctx = ctx(foyer);
|
||||
ctx.getState().set("marlon_following");
|
||||
ctx.getState().set("malte_following");
|
||||
|
||||
assertThat(EscortEngine.followerNote(ctx))
|
||||
.contains("Marlon").contains("Malte").contains("shoulders");
|
||||
}
|
||||
|
||||
@Test
|
||||
void followerNoteIsNullWhenNobodyFollows() {
|
||||
GameContext ctx = ctx(foyer);
|
||||
assertThat(EscortEngine.followerNote(ctx)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reunionInFoyerWithBothFollowingSetsFlagGrantsRewardAndPrintsBeat() {
|
||||
GameContext ctx = ctx(foyer);
|
||||
ctx.getState().set("marlon_following");
|
||||
ctx.getState().set("malte_following");
|
||||
|
||||
EscortEngine.tick(ctx);
|
||||
|
||||
assertThat(ctx.getState().isSet("twins_reunited")).isTrue();
|
||||
assertThat(ctx.getPlayer().hasItem("brass_compass")).isTrue();
|
||||
assertThat(((TestIO) ctx.getIo()).allOutput()).contains("reunited");
|
||||
}
|
||||
|
||||
@Test
|
||||
void followerNoteIsNullOnceReunitedSoTheTwinsAppearGone() {
|
||||
GameContext ctx = ctx(foyer);
|
||||
ctx.getState().set("marlon_following");
|
||||
ctx.getState().set("malte_following");
|
||||
ctx.getState().set("twins_reunited");
|
||||
|
||||
assertThat(EscortEngine.followerNote(ctx)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reunionFiresOnlyOnce() {
|
||||
GameContext ctx = ctx(foyer);
|
||||
ctx.getState().set("marlon_following");
|
||||
ctx.getState().set("malte_following");
|
||||
|
||||
EscortEngine.tick(ctx);
|
||||
EscortEngine.tick(ctx);
|
||||
|
||||
long beats = ((TestIO) ctx.getIo()).outputs().stream()
|
||||
.filter(s -> s.contains("reunited")).count();
|
||||
assertThat(beats).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noReunionWhenOnlyOneTwinFollows() {
|
||||
GameContext ctx = ctx(foyer);
|
||||
ctx.getState().set("marlon_following");
|
||||
|
||||
EscortEngine.tick(ctx);
|
||||
|
||||
assertThat(ctx.getState().isSet("twins_reunited")).isFalse();
|
||||
assertThat(ctx.getPlayer().hasItem("brass_compass")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noReunionWhenBothFollowButNotInFoyer() {
|
||||
GameContext ctx = ctx(conservatory);
|
||||
ctx.getState().set("marlon_following");
|
||||
ctx.getState().set("malte_following");
|
||||
|
||||
EscortEngine.tick(ctx);
|
||||
|
||||
assertThat(ctx.getState().isSet("twins_reunited")).isFalse();
|
||||
assertThat(ctx.getPlayer().hasItem("brass_compass")).isFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user