Sludge is a framework for building text based virtual worlds. It is inspired by old world MUDs, but built with modern JavaScript.
Note: This is very much work in progress. Many of the things outlines below are not yet implemented, and are subject to change at a moment's notice.
Sludge is intended for the rapid creation of virtual worlds. It is designed to be fun to use, hence user productivity is a top priority. Clean mental models and an expressive and intuitive public API are important design objectives. The framework is primarily data driven, with some support for sophisticated users to extend it using custom code.
A Sludge World
is entirely made up of Room
s. These can be subdivided into Area
s, which allow the world to have different biomes with local weather conditions. Rooms are connected by Path
s, which are accessed through endpoints such as Exit
s or Portal
s.
To put a spanner in the works of an unfortunate adventurer, exits can be fitted with furniture such as a Door
or a Gate
, or perhaps something even cooler like a Firewall
. (Not that firewalls are known for being particularly cool.) Furniture can in turn come with further complications, such as a Lock
.
A world would not be very interesting without the ability for characters to act upon it. Whether your inclination is towards exploring, socialising, or just mindless killing, you need the means to express those actions. Sludge uses the concept of Command
s to achieve this. A command is a high level construct used by world builders to bestow efficacy on the players.
Example:
const Knock = Command.register({
guards: [
Guard.actorMustBeAlive,
Guard.actorMustBeAwake,
Guard.directionMustBeValid,
Guard.exitMustExist
]
})
A command is required to pass a number of Guard
s before succeeding. In the case of knocking on a door, as shown above, for the knocking to be successful, the actor must be both alive and awake, the specified direction must be a cardinal direction, and there needs to be an exit leading that way.
A lot of things in the Sludge world are ... well, Thing
s. It is a common enough occurrence that we decided it deserves a first class concept in the framework. Doors, exits, players, demogorgons, and enchanted sticks are all things. Using the Thing
API, we can create factories ready to churn out new things.
Example:
const LightSource = Thing.define({
attributes: {
brightness: 2
},
traits: [
Trait.wieldable
]
})
Trait
s determine how things can be used. In the example above, we have defined a new thing, a light source, which can be wielded by characters (granted they possess the appropriate body parts.) We can use our new thing factory to construct new light sources.
Example:
const candle = Light.build({ brightness: 1 })
Everything that happens in the world is experienced through the eyes (or ears, nose, tendrils, etc.) of a Character
. Each character is equipped with a set of senses which, in addition to an inherent acuity, can be altered through various effects. For example, a dirt kick, or a room may be darkened by some gloomy magic, might temporarily impair a character's sight.
Fear not, however! Even when blinded, you are not rendered unaware of your surroundings. Given that your hearing is still intact, you can get a good sense of the impending doom. Some characters can also have a natural affinity for sensing certain stimuli. For example a tunnel trogg, given it has spent most of its life underground, may have very poor eyesight, but make up for it with a very keen sense of smell.
This is made possible through the emission of SensoryEvent
s. Data packages which can be resolved for each character in the room in which it was emitted (or nearby, in the case of especially powerful events.) A sensory event can target several senses at once, and are resolved in the order of priority inherent to the character's perception. For the sake of sanity, characters only experience sensory events through at most one sense.
Example:
const knock = SensoryEvent.create([
{ sense: "SIGHT", magnitude: 100, message: ({ actor, target }) => `${actor.name} knocks on ${target}` },
{ sense: "HEARING", magnitude: 70, message: ({ target }) => `Someone knocks on ${target}` }
])
In this example, knocking on a door will result in a sensory event targeting sight and hearing inside the room where the knock happens. (So far I am unaware of any cases where someone was able to smell or taste a knock on a door.) Since one can normally not hear who is knocking on a door, the contextual information of the actor is not included in the message targeting hearing. In the case of a knock, we would normally emit an event targeting only hearing to the connecting room as well.
All recipients may not perceive the same event in the same way. Upon resolution, SensoryEvent
s therefore need to be contextualised with the point of view of the character experiencing it. This allows taking into account any circumstances of the recipient.
Example:
const event = SensoryEvent.create([
{ sense: "SIGHT", magnitude: 40, message: "A small imp is hiding in a corner." }
])
const character = Character.build({ senses: { "SIGHT": { acuity: 50 } } })
event.resolve(character)
You can easily run the test suite by invoking npm test
. (This will also run ESLint.)
The framework provides some facilities for writing better, more expressive tests. We can use some of these to test the resolution of sensory events.
Example:
context("when character is powerful enough to sense the presence of an imp", () => {
const event = SensoryEvent.create([
{ sense: "SIGHT", magnitude: 40, message: "A small imp is hiding in a corner." }
])
const character = Character.build({ senses: { "SIGHT": { acuity: 70 } } })
it("is perceived by the character", () => {
event.resolve(character)
expect(character).to.see("A small imp is hiding in a corner.")
})
}
context "when character is not powerful enough to sense the presence of an imp", () => {
const event = SensoryEvent.create([
{ sense: "SIGHT", magnitude: 40, message: "A small imp is hiding in a corner." }
])
const character = Character.build({ senses: { "SIGHT": { acuity: 50 } } })
it("is perceived by the character", () => {
event.resolve(character)
expect(character).to.see.nothing
})
}
LoDash is the single runtime dependency. In development we use Mocha and Chai for testing, and ESLint for keeping things neat.
This project is released under the MIT license. Use whatever you find herein. Go forth, and build great things.