Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"cSpell.words": [
"Bitstream",
"Korgel",
"tadb",
"textadventurejs"
]
}
46 changes: 27 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# TextAdventureJS

A text based adventure engine written in Javascript.
Allows you to create your own text adventure games and embed them into your website or share and play them with included demo player.
A text based adventure engine written in JavaScript.
Create your own text adventure games and embed them into your website, or share and play them with the included demo player.

- Games are json files, that follow the textAdventureGameDatabase schema (tadb).
- Games are json files, that follow the textAdventureGameDatabase-schema (tadb).
- Editor folder contains an editor that allows to create games with a GUI.
- Player folder contains a demo player that shows how to integreate the engine
- Player folder contains a demo player that shows how to integrate the engine

The library and the player are written in pure JavaScript.
The editor uses jQuery.

## Player

<img src="./shared/player.gif" width="400" alt="Animated demo for the textAdventureJS player" />

Try it here: https://dak0r.github.io/TextAdventureJS/player/
Expand All @@ -24,13 +25,16 @@ This repo also provides a full editor including debugger functionality for creat
Try it here: https://dak0r.github.io/TextAdventureJS/editor/

## Usage
Usage is simple: the engine needs to be initilized with a JS functions that allows the engine to write output and to clear all written output. Then any compatible game file can be loaded:

Usage is simple: the engine needs to be initialized with a JS functions that allows the engine to write output and to clear all written output. Then any compatible game file can be loaded:

```js
var textAdvEngine = new textAdventureEngine(writeLine, clearArea);
textAdvEngine.loadDatabaseFromFile(
"https://dak0r.github.io/TextAdventureJS/games/new_project.tadb.json"
);
var textAdvEngine = new textAdventureEngine(writeLine, clearArea);
textAdvEngine.loadDatabaseFromFile(
"https://dak0r.github.io/TextAdventureJS/games/new_project.tadb.json"
);
```

A minimalistic working example, which uses jquery to keep it short:

```html
Expand Down Expand Up @@ -80,13 +84,13 @@ A minimalistic working example, which uses jquery to keep it short:

See [player/index.html](./player/index.html) for a more complex example.

## Game Database
## Game Databases

A Text Adventure Game Database is JSON file which describes games that can run in the TextAdventureJS Engine.
A Text Adventure Game Database is JSON file which describes a game that can run in the TextAdventureJS Engine.

These game files can be validated using the JSON schema in this repo: [textAdventureGameDatabase.schema.json](./textAdventureGameDatabase.schema.json).
Game files are expected to match this JSON schema: [textAdventureGameDatabase.schema.json](./textAdventureGameDatabase.schema.json). The engine is not validating the schema as this would cause external dependencies. If desired, I suggest to handle it in the player, before passing the file or object to the engine.

I recommend using the [JSON Schema Validator](https://marketplace.visualstudio.com/items?itemName=tberman.json-schema-validator), if editing the json files manually.
In VSCode I can recommend using the [JSON Schema Validator](https://marketplace.visualstudio.com/items?itemName=tberman.json-schema-validator) Addon, if editing the JSON files manually. Alternatively [this browser based solution](https://www.jsonschemavalidator.net/) works well, too.

## Concept

Expand All @@ -96,7 +100,7 @@ Each game exists of `objects` which are either in the players inventory or in `l

Locations are basically groups of `objects`.

The description text of a location soley exists of the objects which can be found in it. This means an empty location has no description. Thus a location should always have at least one object, at any given moment.
The description text of a location solely exists of the objects which can be found in it. This means an empty location has no description. Thus a location should always have at least one object, at any given moment.

### Verbs

Expand Down Expand Up @@ -135,14 +139,16 @@ To close the chest again, you can use `objectReplaceInLocation` again in `chest_

#### Object specific failure texts

If the player tries to do soemthing with an object and the action is not defined, the engine will output the default verb failure sentance. In some cases you might find it more immersive to have an object specfic failure text, though verbs have no object specifc failures, as they usually will vary by object.
If the player tries to do something with an object and the action is not defined, the engine will output the default verb failure sentence. In some cases you might find it more immersive to have an object specific failure text, though verbs have no object specific failures, as they usually will vary by object.

So In this case, you simply have to add the verb as an action to the object and add your failure message as text to the action.

### Placeholders

When adding a text to an action, you can use predefined placeholders which will be filled in automatically when the text is written. If the placeholder does not apply in the given context, the value is not replaced.

Existing placeholders are:

- `{verb}` is replaced with the word the player used to describe the verb / action
- `{object}` is replaced with the word the player used to describe the object

Expand All @@ -152,7 +158,7 @@ Commands must be used for any logic that goes beyond outputting text. You can ch

#### 'this' in command parameters

If the command is supposed to affect the object that the action is defined on, you can refer to it using `this` instead of its unique nanme.
If the command is supposed to affect the object that the action is defined on, you can refer to it using `this` instead of its unique name.

#### objectRemoveFromLocation

Expand Down Expand Up @@ -235,8 +241,8 @@ Defined events are:

- `command`: a command was successfully parsed
- `unknown_verb`: the user tried to use a verb that is not defined
- `unknwon_object`: the user tried to use an object that is not present in the players current location or inventory.
- `unkown_verb_for_object`: the user tried to do something with an object that is not defined
- `unknown_object`: the user tried to use an object that is not present in the players current location or inventory.
- `unknown_verb_for_object`: the user tried to do something with an object that is not defined
- `unknown_command`: other parsing error

Each event contains a body, that includes:
Expand All @@ -258,13 +264,15 @@ function analyticsFunction(eventName, eventData) {
## Testing

## Running Editor and Player locally

Running the editor and player html files locally requires a local webserver.
I recommend the `ms-vscode.live-server` extension for vscode.

### Unit Tests

Quick steps to run tests locally:

1. Install Node.js from https://nodejs.org/ if you don't already have it.
2. From the project root run:
- `npm install` (installs dev dependencies like Jest)
- `npm test` (runs the test suite)
- `npm test` (runs the test suite)
8 changes: 4 additions & 4 deletions editor/tba_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ $(document).ready(function () {
) {
e.preventDefault();
if (window.showOpenFilePicker) {
// mimic click handler behaviour but keep it simple for keyboard
// mimic click handler behavior but keep it simple for keyboard
$(".file-drop-area").click();
} else {
$("#fileInput").click();
Expand Down Expand Up @@ -319,10 +319,10 @@ async function getDefaultProjectJson() {
function updateEditorState() {
if (TBA_DATABASE !== undefined) {
$("#drop-area").hide();
$("#editor-aera").show();
$("#editor-area").show();
} else {
$("#drop-area").show();
$("#editor-aera").hide();
$("#editor-area").hide();
}
}

Expand Down Expand Up @@ -1414,7 +1414,7 @@ function generateTextArea(name, value, onChange) {

function getNewVerb(name) {
var newVerb = {};
newVerb["failure"] = "That didnt work.";
newVerb["failure"] = "That didn't work.";
newVerb["words"] = [name];
return newVerb;
}
Expand Down
6 changes: 2 additions & 4 deletions games/new_project.tadb.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@
"text": ["A mysterious object."],
"commands": []
}
},
"useableObjects": {}
}
},
"demo_object_pickedUp": {
"words": ["object", "obj"],
Expand All @@ -154,8 +153,7 @@
"text": ["A mysterious object."],
"commands": []
}
},
"useableObjects": {}
}
},
"door_to_second_room": {
"words": ["door", "north", "up"],
Expand Down
76 changes: 19 additions & 57 deletions games/textadventurejs_teaser.tadb.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,30 +121,6 @@
}
},
"objects": {
"template_object": {
"words": ["desk", "desktop", "workingplace"],
"locationDescription": "",
"actions": {
"pickup": {
"text": ["okay got it"],
"commands": ["inventoryAdd template_object"]
},
"look": {
"text": ["soft and comfy"],
"commands": []
}
},
"useableObjects": {
"bedroom_desk": {
"text": ["soft and comfy"],
"commands": []
},
"test": {
"text": ["works"],
"commands": []
}
}
},
"plaster_wall_door_unidentified": {
"words": ["wall", "walls", "plaster", "metal"],
"locationDescription": "Plaster is crumbling from the WALLS.",
Expand All @@ -165,8 +141,7 @@
"text": ["Why would you do that?"],
"commands": []
}
},
"useableObjects": {}
}
},
"plaster_wall_door_door_visible": {
"words": ["wall", "walls"],
Expand All @@ -187,8 +162,7 @@
"objectRemoveFromLocation metal_behind_plaster"
]
}
},
"useableObjects": {}
}
},
"plaster_wall_door_door_found": {
"words": ["wall", "rusty", "door"],
Expand All @@ -206,8 +180,7 @@
"text": ["You open the rusty door and enter a hallway."],
"commands": ["gotoLocation long_hallway"]
}
},
"useableObjects": {}
}
},
"metal_behind_plaster": {
"words": ["rust", "rusty", "metal", "plaster"],
Expand All @@ -234,8 +207,7 @@
"objectRemoveFromLocation metal_behind_plaster"
]
}
},
"useableObjects": {}
}
},
"room_start_chamber_dark": {
"words": ["room"],
Expand All @@ -253,10 +225,9 @@
"text": ["If only you'd had something to enlighten the room with..."],
"commands": []
}
},
"useableObjects": {}
}
},
"room_start_chamber_enlighted": {
"room_start_chamber_enlightened": {
"words": ["room"],
"locationDescription": "The room is slightly lit.",
"actions": {
Expand All @@ -268,8 +239,7 @@
"text": ["The room is already slightly illuminated."],
"commands": []
}
},
"useableObjects": {}
}
},
"box_matches_unidentified": {
"words": ["box"],
Expand All @@ -288,31 +258,30 @@
],
"commands": []
}
},
"useableObjects": {}
}
},
"box_matches_pickedUp": {
"words": ["box", "matches", "matchbox", "match"],
"locationDescription": "You are holding the box of MATCHES.",
"actions": {
"use": {
"text": [
"You light the last match. The room is now slightly illumanted."
"You light the last match. The room is now slightly illuminated."
],
"commands": [
"objectReplaceInLocation plaster_wall_door_unidentified plaster_wall_door_door_visible",
"objectReplaceInLocation room_start_chamber_dark room_start_chamber_enlighted",
"objectReplaceInLocation room_start_chamber_dark room_start_chamber_enlightened",
"objectAddToLocation metal_behind_plaster",
"inventoryRemove box_matches_pickedUp"
]
},
"light": {
"text": [
"You light the last match. The room is now slightly illumanted."
"You light the last match. The room is now slightly illuminated."
],
"commands": [
"objectReplaceInLocation plaster_wall_door_unidentified plaster_wall_door_door_visible",
"objectReplaceInLocation room_start_chamber_dark room_start_chamber_enlighted",
"objectReplaceInLocation room_start_chamber_dark room_start_chamber_enlightened",
"objectAddToLocation metal_behind_plaster",
"inventoryRemove box_matches_pickedUp"
]
Expand All @@ -328,8 +297,7 @@
"text": ["An almost empty box of matches."],
"commands": []
}
},
"useableObjects": {}
}
},
"box_matches_dropped": {
"words": ["box", "matches", "matchbox"],
Expand All @@ -346,8 +314,7 @@
"text": ["A small box of matches, that you dropped here..."],
"commands": []
}
},
"useableObjects": {}
}
},
"room_hallway": {
"words": ["room", "hallway"],
Expand All @@ -357,8 +324,7 @@
"text": [""],
"commands": []
}
},
"useableObjects": {}
}
},
"exit_light": {
"words": ["light", "exit"],
Expand All @@ -376,8 +342,7 @@
],
"commands": []
}
},
"useableObjects": {}
}
},
"exit_light_without_leaflet": {
"words": ["light", "exit"],
Expand All @@ -395,8 +360,7 @@
],
"commands": []
}
},
"useableObjects": {}
}
},
"dev_leaflet_on_ground": {
"words": ["leaflet", "notice", "paper"],
Expand All @@ -416,8 +380,7 @@
"inventoryAdd dev_leaflet_pickedUp"
]
}
},
"useableObjects": {}
}
},
"dev_leaflet_pickedUp": {
"words": ["leaflet", "notice", "paper"],
Expand All @@ -438,8 +401,7 @@
"inventoryRemove dev_leaflet_pickedUp"
]
}
},
"useableObjects": {}
}
}
},
"locations": {
Expand Down
Loading