Compare commits
1 Commits
main
...
99d957b461
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99d957b461 |
@@ -61,35 +61,6 @@ jobs:
|
|||||||
set -e
|
set -e
|
||||||
[[ $r -eq 0 || $r -eq 255 ]]
|
[[ $r -eq 0 || $r -eq 255 ]]
|
||||||
|
|
||||||
- name: Smoke - weather and time
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
|
||||||
MUD_PID=$!
|
|
||||||
trap 'kill $MUD_PID 2>/dev/null || true' EXIT
|
|
||||||
bash scripts/ci/wait-for-tcp.sh 127.0.0.1 2222
|
|
||||||
set +e
|
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF' > weather_test.out
|
|
||||||
go south
|
|
||||||
go down
|
|
||||||
look
|
|
||||||
quit
|
|
||||||
EOF
|
|
||||||
r=$?
|
|
||||||
set -e
|
|
||||||
[[ $r -eq 0 || $r -eq 255 ]]
|
|
||||||
if ! grep -q "The sky is\|raining\|storm\|snow\|fog" weather_test.out; then
|
|
||||||
echo "Error: Weather info not found in look output"
|
|
||||||
cat weather_test.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! grep -q "\[Night\]\|\[Morning\]\|\[Afternoon\]\|\[Evening\]" weather_test.out; then
|
|
||||||
echo "Error: Time of day info not found in look output"
|
|
||||||
cat weather_test.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm weather_test.out
|
|
||||||
|
|
||||||
- name: Smoke - persistence (reconnect)
|
- name: Smoke - persistence (reconnect)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -208,24 +179,37 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
MS_LOG=$(ls -t logs/mudserver_*.log 2>/dev/null | head -n 1)
|
||||||
|
CB_LOG=$(ls -t logs/combat_*.log 2>/dev/null | head -n 1)
|
||||||
|
|
||||||
FAILED=0
|
FAILED=0
|
||||||
|
|
||||||
echo "Checking mudserver logs..."
|
if [ -z "$MS_LOG" ]; then
|
||||||
grep -q "World '.*': .* rooms" logs/mudserver_*.log || { echo "Failed: World loading log missing"; FAILED=1; }
|
echo "Error: no mudserver log files found"
|
||||||
grep -q "MUD server listening on" logs/mudserver_*.log || { echo "Failed: Listen log missing"; FAILED=1; }
|
FAILED=1
|
||||||
grep -q "New character created: smoketest" logs/mudserver_*.log || { echo "Failed: smoketest creation log missing"; FAILED=1; }
|
else
|
||||||
grep -q "Admin action: registration setting updated: '.*'" logs/mudserver_*.log || { echo "Failed: Admin action log missing"; FAILED=1; }
|
echo "Checking mudserver log: $MS_LOG"
|
||||||
|
grep -q "World '.*': .* rooms" "$MS_LOG" || { echo "Failed: World loading log missing"; FAILED=1; }
|
||||||
|
grep -q "MUD server listening on" "$MS_LOG" || { echo "Failed: Listen log missing"; FAILED=1; }
|
||||||
|
grep -q "New character created: smoketest" "$MS_LOG" || { echo "Failed: smoketest creation log missing"; FAILED=1; }
|
||||||
|
grep -q "Admin action: registration setting updated: '.*'" "$MS_LOG" || { echo "Failed: Admin action log missing"; FAILED=1; }
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Checking combat logs..."
|
if [ -z "$CB_LOG" ]; then
|
||||||
grep -q "Combat: Player 'smoketest' (ID .*) engaged NPC 'Shadowy Thief'" logs/combat_*.log || { echo "Failed: Combat engagement log missing"; FAILED=1; }
|
echo "Error: no combat log files found"
|
||||||
grep -q "Combat: Player 'smoketest' (ID .*) killed NPC 'Shadowy Thief'" logs/combat_*.log || { echo "Failed: NPC kill log missing"; FAILED=1; }
|
FAILED=1
|
||||||
|
else
|
||||||
|
echo "Checking combat log: $CB_LOG"
|
||||||
|
grep -q "Combat: Player 'smoketest' (ID .*) engaged NPC 'Shadowy Thief'" "$CB_LOG" || { echo "Failed: Combat engagement log missing"; FAILED=1; }
|
||||||
|
grep -q "Combat: Player 'smoketest' (ID .*) killed NPC 'Shadowy Thief'" "$CB_LOG" || { echo "Failed: NPC kill log missing"; FAILED=1; }
|
||||||
|
fi
|
||||||
|
|
||||||
if [ $FAILED -ne 0 ]; then
|
if [ $FAILED -ne 0 ]; then
|
||||||
echo "--- LOG VERIFICATION FAILED ---"
|
echo "--- LOG VERIFICATION FAILED ---"
|
||||||
echo "--- MUDSERVER LOG CONTENTS ---"
|
echo "--- MUDSERVER LOG CONTENTS ---"
|
||||||
cat logs/mudserver_*.log
|
[ -n "$MS_LOG" ] && cat "$MS_LOG"
|
||||||
echo "--- COMBAT LOG CONTENTS ---"
|
echo "--- COMBAT LOG CONTENTS ---"
|
||||||
cat logs/combat_*.log
|
[ -n "$CB_LOG" ] && cat "$CB_LOG"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Logging verification passed."
|
echo "Logging verification passed."
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
- **Shops / economy** — NPCs that buy and sell; currency and pricing.
|
- **Shops / economy** — NPCs that buy and sell; currency and pricing.
|
||||||
- **Enhanced NPC Interactions** — Keyword-based dialogue system.
|
- **Enhanced NPC Interactions** — Keyword-based dialogue system.
|
||||||
- **Aggressive NPC AI** — NPCs with Aggressive attitude now correctly initiate combat.
|
- **Aggressive NPC AI** — NPCs with Aggressive attitude now correctly initiate combat.
|
||||||
- **Weather** — Weather system (e.g., rain, snow, fog) affecting areas or atmosphere.
|
|
||||||
- **Day/night or time of day** — Time cycle affecting room descriptions, spawns, or NPC behavior.
|
|
||||||
|
|
||||||
## Easy
|
## Easy
|
||||||
|
|
||||||
@@ -20,7 +18,8 @@ Content-only or minimal code; add TOML/data and existing systems already support
|
|||||||
|
|
||||||
New state, commands, or mechanics with bounded scope.
|
New state, commands, or mechanics with bounded scope.
|
||||||
|
|
||||||
- **Robust Logging** — Structured logging, rotation, and persistence; better visibility into server state and player actions.
|
- **Weather** — Weather system (e.g., rain, snow, fog) affecting areas or atmosphere; scope TBD.
|
||||||
|
- **Day/night or time of day** — Time cycle affecting room descriptions, spawns, or NPC behavior; lighter than full weather.
|
||||||
- **Quests or objectives** — Simple “kill X” / “bring Y” goals; quest state in DB and hooks in combat/loot/NPCs.
|
- **Quests or objectives** — Simple “kill X” / “bring Y” goals; quest state in DB and hooks in combat/loot/NPCs.
|
||||||
- **Player parties** — Group formation, shared objectives, party-only chat or visibility; new state and commands.
|
- **Player parties** — Group formation, shared objectives, party-only chat or visibility; new state and commands.
|
||||||
- **PvP** — Player-vs-player combat; consent/flagging, safe zones, and balance TBD.
|
- **PvP** — Player-vs-player combat; consent/flagging, safe zones, and balance TBD.
|
||||||
|
|||||||
@@ -113,13 +113,6 @@ Run through the checks below before every commit to ensure consistent feature co
|
|||||||
- [ ] Negative status effects cleared on player death/respawn
|
- [ ] Negative status effects cleared on player death/respawn
|
||||||
- [ ] Status effects on offline players resolve by wall-clock time on next login
|
- [ ] Status effects on offline players resolve by wall-clock time on next login
|
||||||
|
|
||||||
## Weather & Time
|
|
||||||
- [ ] Outdoor rooms display time of day (e.g., `[Night]`, `[Morning]`).
|
|
||||||
- [ ] Outdoor rooms display current weather (e.g., `The sky is clear`, `It is raining`).
|
|
||||||
- [ ] Indoor rooms do not show weather or time of day.
|
|
||||||
- [ ] Rain or storm applies the `wet` status effect to players in outdoor rooms.
|
|
||||||
- [ ] Weather changes periodically and broadcasts messages to players in outdoor rooms.
|
|
||||||
|
|
||||||
## Guilds
|
## Guilds
|
||||||
- [ ] `guild list` shows all available guilds with descriptions
|
- [ ] `guild list` shows all available guilds with descriptions
|
||||||
- [ ] `guild info <name>` shows guild details, growth stats, and spell list
|
- [ ] `guild info <name>` shows guild details, growth stats, and spell list
|
||||||
|
|||||||
BIN
mudserver.db.test
Normal file
BIN
mudserver.db.test
Normal file
Binary file not shown.
BIN
mudserver.db.test-shm
Normal file
BIN
mudserver.db.test-shm
Normal file
Binary file not shown.
BIN
mudserver.db.test-wal
Normal file
BIN
mudserver.db.test-wal
Normal file
Binary file not shown.
19
run-tests.sh
19
run-tests.sh
@@ -44,25 +44,6 @@ flee
|
|||||||
quit
|
quit
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
ssh_mud smoketest@localhost <<'EOF' > weather_test.out
|
|
||||||
go south
|
|
||||||
go down
|
|
||||||
look
|
|
||||||
quit
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if ! grep -q "The sky is\|raining\|storm\|snow\|fog" weather_test.out; then
|
|
||||||
echo "Error: Weather info not found in look output"
|
|
||||||
cat weather_test.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! grep -q "\[Night\]\|\[Morning\]\|\[Afternoon\]\|\[Evening\]" weather_test.out; then
|
|
||||||
echo "Error: Time of day info not found in look output"
|
|
||||||
cat weather_test.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm weather_test.out
|
|
||||||
|
|
||||||
ssh_mud smoketest@localhost <<'EOF'
|
ssh_mud smoketest@localhost <<'EOF'
|
||||||
look
|
look
|
||||||
stats
|
stats
|
||||||
|
|||||||
@@ -180,14 +180,6 @@ fn attitude_color(att: Attitude) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_time_of_day(tick: u64) -> &'static str {
|
|
||||||
let day_tick = tick % 1440;
|
|
||||||
if day_tick < 360 { "Night" }
|
|
||||||
else if day_tick < 720 { "Morning" }
|
|
||||||
else if day_tick < 1080 { "Afternoon" }
|
|
||||||
else { "Evening" }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_room_view(
|
pub fn render_room_view(
|
||||||
room_id: &str,
|
room_id: &str,
|
||||||
player_id: usize,
|
player_id: usize,
|
||||||
@@ -203,19 +195,13 @@ pub fn render_room_view(
|
|||||||
.map(|c| c.player.name.as_str())
|
.map(|c| c.player.name.as_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
let time_of_day = get_time_of_day(st.tick_count);
|
|
||||||
let mut out = format!(
|
let mut out = format!(
|
||||||
"\r\n{} {} {}\r\n {}\r\n",
|
"\r\n{} {}\r\n {}\r\n",
|
||||||
ansi::room_name(&room.name),
|
ansi::room_name(&room.name),
|
||||||
ansi::system_msg(&format!("[{}]", room.region)),
|
ansi::system_msg(&format!("[{}]", room.region)),
|
||||||
ansi::color(ansi::YELLOW, &format!("[{}]", time_of_day)),
|
|
||||||
room.description
|
room.description
|
||||||
);
|
);
|
||||||
|
|
||||||
if room.outdoors {
|
|
||||||
out.push_str(&format!(" {}\r\n", ansi::color(ansi::CYAN, st.weather.kind.description())));
|
|
||||||
}
|
|
||||||
|
|
||||||
let npc_strs: Vec<String> = room
|
let npc_strs: Vec<String> = room
|
||||||
.npcs
|
.npcs
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
34
src/game.rs
34
src/game.rs
@@ -9,35 +9,6 @@ use russh::ChannelId;
|
|||||||
use crate::db::{GameDb, SavedPlayer};
|
use crate::db::{GameDb, SavedPlayer};
|
||||||
use crate::world::{Attitude, Class, Object, Race, World};
|
use crate::world::{Attitude, Class, Object, Race, World};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum WeatherKind {
|
|
||||||
Clear,
|
|
||||||
Cloudy,
|
|
||||||
Rain,
|
|
||||||
Storm,
|
|
||||||
Snow,
|
|
||||||
Fog,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WeatherKind {
|
|
||||||
pub fn description(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
WeatherKind::Clear => "The sky is clear.",
|
|
||||||
WeatherKind::Cloudy => "The sky is overcast with clouds.",
|
|
||||||
WeatherKind::Rain => "It is raining.",
|
|
||||||
WeatherKind::Storm => "A powerful storm is raging.",
|
|
||||||
WeatherKind::Snow => "Soft snowflakes are falling from the sky.",
|
|
||||||
WeatherKind::Fog => "A thick fog blankets the area.",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct WeatherState {
|
|
||||||
pub kind: WeatherKind,
|
|
||||||
pub remaining_ticks: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlayerStats {
|
pub struct PlayerStats {
|
||||||
pub max_hp: i32,
|
pub max_hp: i32,
|
||||||
@@ -171,7 +142,6 @@ pub struct GameState {
|
|||||||
pub npc_instances: HashMap<String, NpcInstance>,
|
pub npc_instances: HashMap<String, NpcInstance>,
|
||||||
pub rng: XorShift64,
|
pub rng: XorShift64,
|
||||||
pub tick_count: u64,
|
pub tick_count: u64,
|
||||||
pub weather: WeatherState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SharedState = Arc<Mutex<GameState>>;
|
pub type SharedState = Arc<Mutex<GameState>>;
|
||||||
@@ -256,10 +226,6 @@ impl GameState {
|
|||||||
npc_instances,
|
npc_instances,
|
||||||
rng,
|
rng,
|
||||||
tick_count: 0,
|
tick_count: 0,
|
||||||
weather: WeatherState {
|
|
||||||
kind: WeatherKind::Clear,
|
|
||||||
remaining_ticks: 100,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ async fn main() {
|
|||||||
Naming::Numbers,
|
Naming::Numbers,
|
||||||
Cleanup::KeepLogFiles(7),
|
Cleanup::KeepLogFiles(7),
|
||||||
)
|
)
|
||||||
.append()
|
|
||||||
.write_mode(WriteMode::Direct)
|
.write_mode(WriteMode::Direct)
|
||||||
.try_build()
|
.try_build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -100,7 +99,6 @@ async fn main() {
|
|||||||
Logger::try_with_str(&log_level)
|
Logger::try_with_str(&log_level)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.log_to_file(FileSpec::default().directory(&log_dir).basename("mudserver"))
|
.log_to_file(FileSpec::default().directory(&log_dir).basename("mudserver"))
|
||||||
.append()
|
|
||||||
.duplicate_to_stderr(Duplicate::All)
|
.duplicate_to_stderr(Duplicate::All)
|
||||||
.rotate(
|
.rotate(
|
||||||
Criterion::Size(10_000_000), // 10 MB
|
Criterion::Size(10_000_000), // 10 MB
|
||||||
|
|||||||
70
src/tick.rs
70
src/tick.rs
@@ -26,48 +26,6 @@ pub async fn run_tick_engine(state: SharedState) {
|
|||||||
st.tick_count += 1;
|
st.tick_count += 1;
|
||||||
let tick = st.tick_count;
|
let tick = st.tick_count;
|
||||||
|
|
||||||
let mut weather_msg = None;
|
|
||||||
st.weather.remaining_ticks = st.weather.remaining_ticks.saturating_sub(1);
|
|
||||||
if st.weather.remaining_ticks == 0 {
|
|
||||||
let old_kind = st.weather.kind;
|
|
||||||
st.weather.kind = match st.rng.next_range(0, 5) {
|
|
||||||
0 => crate::game::WeatherKind::Clear,
|
|
||||||
1 => crate::game::WeatherKind::Cloudy,
|
|
||||||
2 => crate::game::WeatherKind::Rain,
|
|
||||||
3 => crate::game::WeatherKind::Storm,
|
|
||||||
4 => crate::game::WeatherKind::Snow,
|
|
||||||
5 => crate::game::WeatherKind::Fog,
|
|
||||||
_ => crate::game::WeatherKind::Clear,
|
|
||||||
};
|
|
||||||
st.weather.remaining_ticks = st.rng.next_range(100, 400) as u32;
|
|
||||||
|
|
||||||
if old_kind != st.weather.kind {
|
|
||||||
weather_msg = Some(format!(
|
|
||||||
"\r\n {}\r\n",
|
|
||||||
ansi::color(ansi::CYAN, st.weather.kind.description())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply "wet" effect if raining/storming
|
|
||||||
if st.weather.kind == crate::game::WeatherKind::Rain
|
|
||||||
|| st.weather.kind == crate::game::WeatherKind::Storm
|
|
||||||
{
|
|
||||||
let wet_players: Vec<String> = st
|
|
||||||
.players
|
|
||||||
.values()
|
|
||||||
.filter_map(|c| {
|
|
||||||
st.world
|
|
||||||
.get_room(&c.player.room_id)
|
|
||||||
.filter(|r| r.outdoors)
|
|
||||||
.map(|_| c.player.name.clone())
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
for name in wet_players {
|
|
||||||
st.db.save_effect(&name, "wet", 10, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
st.check_respawns();
|
st.check_respawns();
|
||||||
|
|
||||||
// --- NPC auto-aggro: hostile NPCs initiate combat with players in their room ---
|
// --- NPC auto-aggro: hostile NPCs initiate combat with players in their room ---
|
||||||
@@ -102,16 +60,6 @@ pub async fn run_tick_engine(state: SharedState) {
|
|||||||
|
|
||||||
let mut messages: HashMap<usize, String> = HashMap::new();
|
let mut messages: HashMap<usize, String> = HashMap::new();
|
||||||
|
|
||||||
if let Some(msg) = weather_msg {
|
|
||||||
for (&pid, conn) in st.players.iter() {
|
|
||||||
if let Some(room) = st.world.get_room(&conn.player.room_id) {
|
|
||||||
if room.outdoors {
|
|
||||||
messages.entry(pid).or_default().push_str(&msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (pid, npc_id) in &new_combats {
|
for (pid, npc_id) in &new_combats {
|
||||||
let npc_name = st
|
let npc_name = st
|
||||||
.world
|
.world
|
||||||
@@ -235,24 +183,6 @@ pub async fn run_tick_engine(state: SharedState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"wet" => {
|
|
||||||
let online_pid = st
|
|
||||||
.players
|
|
||||||
.iter()
|
|
||||||
.find(|(_, c)| c.player.name == eff.player_name)
|
|
||||||
.map(|(&id, _)| id);
|
|
||||||
|
|
||||||
if let Some(pid) = online_pid {
|
|
||||||
if let Some(_conn) = st.players.get_mut(&pid) {
|
|
||||||
if eff.remaining_ticks <= 0 {
|
|
||||||
messages.entry(pid).or_default().push_str(&format!(
|
|
||||||
"\r\n {} You dry off.\r\n",
|
|
||||||
ansi::color(ansi::CYAN, "~~"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"regen" => {
|
"regen" => {
|
||||||
let heal = eff.magnitude;
|
let heal = eff.magnitude;
|
||||||
let online_pid = st
|
let online_pid = st
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ pub struct RoomFile {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub exits: HashMap<String, String>,
|
pub exits: HashMap<String, String>,
|
||||||
#[serde(default)]
|
|
||||||
pub outdoors: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
@@ -415,7 +413,6 @@ pub struct Room {
|
|||||||
pub exits: HashMap<String, String>,
|
pub exits: HashMap<String, String>,
|
||||||
pub npcs: Vec<String>,
|
pub npcs: Vec<String>,
|
||||||
pub objects: Vec<String>,
|
pub objects: Vec<String>,
|
||||||
pub outdoors: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -668,7 +665,7 @@ impl World {
|
|||||||
|
|
||||||
load_entities_from_dir(®ion_path.join("rooms"), ®ion_name, &mut |id, content| {
|
load_entities_from_dir(®ion_path.join("rooms"), ®ion_name, &mut |id, content| {
|
||||||
let rf: RoomFile = toml::from_str(content).map_err(|e| format!("Bad room {id}: {e}"))?;
|
let rf: RoomFile = toml::from_str(content).map_err(|e| format!("Bad room {id}: {e}"))?;
|
||||||
rooms.insert(id.clone(), Room { id: id.clone(), region: region_name.clone(), name: rf.name, description: rf.description, exits: rf.exits, npcs: Vec::new(), objects: Vec::new(), outdoors: rf.outdoors });
|
rooms.insert(id.clone(), Room { id: id.clone(), region: region_name.clone(), name: rf.name, description: rf.description, exits: rf.exits, npcs: Vec::new(), objects: Vec::new() });
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ description = """\
|
|||||||
Towering iron-reinforced wooden gates mark the southern edge of town. \
|
Towering iron-reinforced wooden gates mark the southern edge of town. \
|
||||||
Guards in dented armor lean on their spears, watching the dusty road \
|
Guards in dented armor lean on their spears, watching the dusty road \
|
||||||
that stretches into the wilderness beyond."""
|
that stretches into the wilderness beyond."""
|
||||||
outdoors = true
|
|
||||||
|
|
||||||
[exits]
|
[exits]
|
||||||
north = "town:dark_alley"
|
north = "town:dark_alley"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ description = """\
|
|||||||
Colorful stalls line both sides of a narrow street. Merchants hawk their \
|
Colorful stalls line both sides of a narrow street. Merchants hawk their \
|
||||||
wares — bolts of cloth, exotic spices, gleaming trinkets. The air is thick \
|
wares — bolts of cloth, exotic spices, gleaming trinkets. The air is thick \
|
||||||
with competing smells and the chatter of commerce."""
|
with competing smells and the chatter of commerce."""
|
||||||
outdoors = true
|
|
||||||
|
|
||||||
[exits]
|
[exits]
|
||||||
west = "town:town_square"
|
west = "town:town_square"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ description = """\
|
|||||||
You stand in the heart of Thornwall. A worn stone fountain sits at the \
|
You stand in the heart of Thornwall. A worn stone fountain sits at the \
|
||||||
center, water trickling quietly. Cobblestone paths branch in every \
|
center, water trickling quietly. Cobblestone paths branch in every \
|
||||||
direction. The sounds of merchants and travelers fill the air."""
|
direction. The sounds of merchants and travelers fill the air."""
|
||||||
outdoors = true
|
|
||||||
|
|
||||||
[exits]
|
[exits]
|
||||||
north = "town:tavern"
|
north = "town:tavern"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ The well-maintained cobblestone of Thornwall yields to a winding dirt \
|
|||||||
path that disappears into the dense, dark eaves of the forest. The air \
|
path that disappears into the dense, dark eaves of the forest. The air \
|
||||||
is cooler here, smelling of damp earth and pine needles. The city \
|
is cooler here, smelling of damp earth and pine needles. The city \
|
||||||
gates loom to the north."""
|
gates loom to the north."""
|
||||||
outdoors = true
|
|
||||||
|
|
||||||
[exits]
|
[exits]
|
||||||
north = "town:gate"
|
north = "town:gate"
|
||||||
|
|||||||
Reference in New Issue
Block a user