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
|
## Schnellstart ohne Maven
|
||||||
|
|
||||||
Im Ordner [`release/`](release/) liegt eine fertig gebaute, eigenständige JAR
|
Im Ordner [`release/`](release/) liegen zwei fertig gebaute, eigenständige
|
||||||
(`HauntedManor.jar`) mit allen Abhängigkeiten. Sie braucht nur Java 25:
|
JARs mit allen Abhängigkeiten – je eine pro Frontend. Sie brauchen nur Java 25:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar release/HauntedManor.jar # Konsolen-Version
|
java -jar release/HauntedManor-Console.jar # Konsolen-Version (~5 MB, ohne Audio)
|
||||||
java -cp release/HauntedManor.jar thb.jeanluc.adventure.AppGui # Swing-GUI
|
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
|
## Bauen und Testen
|
||||||
|
|
||||||
Mit dem Wrapper (empfohlen, keine Maven-Installation nötig) – unter Windows
|
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
|
```bash
|
||||||
./mvnw clean test # alle Tests ausführen (233 Tests)
|
./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` …).
|
Ein systemweit installiertes `mvn` funktioniert identisch (`mvn clean test` …).
|
||||||
|
|||||||
@@ -113,25 +113,65 @@
|
|||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
<version>3.6.0</version>
|
<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>
|
<executions>
|
||||||
|
<!-- Console version -> release/HauntedManor-Console.jar -->
|
||||||
<execution>
|
<execution>
|
||||||
|
<id>shade-console</id>
|
||||||
<phase>package</phase>
|
<phase>package</phase>
|
||||||
<goals>
|
<goals>
|
||||||
<goal>shade</goal>
|
<goal>shade</goal>
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<!-- Self-contained, runnable JAR with all dependencies bundled. -->
|
<outputFile>${project.basedir}/release/HauntedManor-Console.jar</outputFile>
|
||||||
<outputFile>${project.basedir}/release/HauntedManor.jar</outputFile>
|
<!-- Console has no audio (GameIO.setMusic is a no-op there), so leave
|
||||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
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>
|
<transformers>
|
||||||
<!-- Default entry point: console version. GUI via -cp ... AppGui. -->
|
|
||||||
<transformer
|
<transformer
|
||||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<manifestEntries>
|
<manifestEntries>
|
||||||
<Main-Class>thb.jeanluc.adventure.App</Main-Class>
|
<Main-Class>thb.jeanluc.adventure.App</Main-Class>
|
||||||
</manifestEntries>
|
</manifestEntries>
|
||||||
</transformer>
|
</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
|
<transformer
|
||||||
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||||
</transformers>
|
</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.command.Command;
|
||||||
import thb.jeanluc.adventure.game.Conditions;
|
import thb.jeanluc.adventure.game.Conditions;
|
||||||
|
import thb.jeanluc.adventure.game.EscortEngine;
|
||||||
import thb.jeanluc.adventure.game.GameContext;
|
import thb.jeanluc.adventure.game.GameContext;
|
||||||
import thb.jeanluc.adventure.game.Light;
|
import thb.jeanluc.adventure.game.Light;
|
||||||
import thb.jeanluc.adventure.io.text.RoomView;
|
import thb.jeanluc.adventure.io.text.RoomView;
|
||||||
@@ -36,6 +37,10 @@ public class LookCommand implements Command {
|
|||||||
break;
|
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));
|
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() {
|
private void publishHud() {
|
||||||
QuestEngine.tick(ctx);
|
QuestEngine.tick(ctx);
|
||||||
|
EscortEngine.tick(ctx);
|
||||||
ctx.getIo().setHud(new Hud(
|
ctx.getIo().setHud(new Hud(
|
||||||
ctx.getPlayer().getCurrentRoom().getName(),
|
ctx.getPlayer().getCurrentRoom().getName(),
|
||||||
ctx.getPlayer().getGold(),
|
ctx.getPlayer().getGold(),
|
||||||
|
|||||||
@@ -147,4 +147,14 @@ public class Room {
|
|||||||
public Optional<Npc> findNpc(String npcId) {
|
public Optional<Npc> findNpc(String npcId) {
|
||||||
return Optional.ofNullable(npcs.get(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
|
# 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.
|
# 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
|
- id: spirit_at_rest
|
||||||
title: "Spirit at Rest"
|
title: "Spirit at Rest"
|
||||||
victory: true
|
victory: true
|
||||||
|
|||||||
@@ -122,3 +122,27 @@
|
|||||||
id: shovel
|
id: shovel
|
||||||
name: Shovel
|
name: Shovel
|
||||||
description: A rusty shovel. Sturdy enough to dig, though nothing here needs digging.
|
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
|
consumes: letter
|
||||||
effects:
|
effects:
|
||||||
- { setFlag: heard_tale }
|
- { 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:
|
stages:
|
||||||
- objective: "Lay the restless spirit to rest in the chapel."
|
- objective: "Lay the restless spirit to rest in the chapel."
|
||||||
completion: [{ flag: ghost_banished }]
|
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: |
|
description: |
|
||||||
A glass-roofed room choked with dead ferns. Rusted plumbing lines the
|
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.
|
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
|
music: manor-theme.ogg
|
||||||
exits:
|
exits:
|
||||||
south: dining_room
|
south: dining_room
|
||||||
items: [valve_wheel]
|
items: [valve_wheel, marlon_keepsake]
|
||||||
npcs: []
|
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
|
- id: grand_staircase
|
||||||
name: Grand Staircase
|
name: Grand Staircase
|
||||||
@@ -135,13 +142,20 @@
|
|||||||
name: Boiler Room
|
name: Boiler Room
|
||||||
description: |
|
description: |
|
||||||
A cramped stone room behind the furnace. A rusty generator squats in the
|
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
|
music: cellar-drone.ogg
|
||||||
dark: true
|
dark: true
|
||||||
exits:
|
exits:
|
||||||
east: cellar_stairs
|
east: cellar_stairs
|
||||||
items: [generator]
|
items: [generator, malte_keepsake]
|
||||||
npcs: []
|
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) ───────────────────────
|
# ── 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