diff --git a/game-disks/demo-disk.js b/game-disks/demo-disk.js
index 6852a6d..e724067 100644
--- a/game-disks/demo-disk.js
+++ b/game-disks/demo-disk.js
@@ -1,4 +1,4 @@
-const demoDisk = {
+const demoDisk = () => ({
roomId: 'foyer', // the ID of the room the player starts in
rooms: [
{
@@ -14,7 +14,7 @@ const demoDisk = {
const room = getRoom('foyer');
room.desc = `You are currently standing in the foyer. There's a huge **MONSTERA** plant to your right, and a massive **WINDOW** to your left bathing the room in natural light. Both the **PLANT** and the **WINDOW** stretch to the ceiling, which must be at least 25 feet high.
- ***Rooms** form the foundation of the engine's design. At any given time, your player will be standing in one of the rooms you built for them. These can be literal rooms like the foyer you find yourself in now, or metaphorical rooms like **The End of Time** or **Purgatory**.
+ ***Rooms** form the foundation of the engine's design. At any given time, your player will be standing in one of the rooms you built for them. These can be literal rooms like the foyer you find yourself in now, or metaphorical rooms like **The End of Time** or **A Dream**.
Each room you create should have a description. (That's what you're reading now!)
@@ -38,7 +38,7 @@ const demoDisk = {
block: `It's far too large for you to carry.`, // optional reason player cannot pick up this item
// when player looks at the plant, they discover a shiny object which turns out to be a key
onLook: () => {
- if (getItemInRoom('shiny', 'foyer') || getItemInInventory('shiny')) {
+ if (getItem('shiny')) {
// the key is already in the pot or the player's inventory
return;
}
@@ -48,7 +48,7 @@ const demoDisk = {
// put the silver key in the pot
foyer.items.push({
name: ['shiny thing', 'something shiny', 'pot'],
- onUse: () => {
+ onUse() {
const room = getRoom(disk.roomId);
if (room.id === 'foyer') {
println(`There's nothing to unlock in the foyer.`);
@@ -58,15 +58,15 @@ const demoDisk = {
const exit = getExit('east', room.exits);
delete exit.block;
// this item can only be used once
- const key = getItemInInventory('shiny');
+ const key = getItem('shiny');
key.onUse = () => println(`The lab has already been unlocked.`);
} else {
println(`There's nothing to unlock here.`);
}
},
desc: `It's a silver **KEY**!`,
- onLook: () => {
- const key = getItemInInventory('shiny') || getItemInRoom('shiny', 'foyer');
+ onLook() {
+ const key = getItem('shiny');
// now that we know it's a key, place that name first so the engine calls it by that name
key.name.unshift('silver key');
@@ -78,10 +78,10 @@ const demoDisk = {
delete key.onLook;
},
isTakeable: true,
- onTake: () => {
+ onTake() {
println(`You took it.`);
// update the monstera's description, removing everything starting at the line break
- const plant = getItemInRoom('plant', 'foyer');
+ const plant = getItem('plant');
plant.desc = plant.desc.slice(0, plant.desc.indexOf('\n'));
},
});
@@ -97,7 +97,7 @@ const demoDisk = {
Type **INV** to see a list of items in your inventory.*`),
// using the dime randomly prints HEADS or TAILS
- onUse: () => {
+ onUse() {
const side = Math.random() > 0.5 ? 'HEADS' : 'TAILS';
println(`You flip the dime. It lands on ${side}.`);
},
@@ -128,7 +128,7 @@ const demoDisk = {
{
name: 'door',
desc: `There are 4" metal letters nailed to the door. They spell out: "RESEARCH LAB".`,
- onUse: () => {
+ onUse() {
const reception = getRoom('reception');
const exit = getExit('east', reception.exits);
if (exit.block) {
@@ -195,7 +195,7 @@ const demoDisk = {
// add a special item to the player's inventory
disk.inventory.push({
- name: 'style-changer',
+ name: ['style-changer', 'stylechanger'],
desc: `This is a magical item. Type **USE STYLE-CHANGER** to try it out!`,
onUse: () => {
const currentStylesheet = document.getElementById('styles').getAttribute('href');
@@ -275,7 +275,7 @@ const demoDisk = {
},
{
option: `What is a **DISK**?`,
- line: `A disk is a JavaScript object which describes your game. At minimum, it must have these two top-level properties:
+ line: `A disk is a JavaScript function returning an object which describes your game. At minimum, the returned object must have these two top-level properties:
**roomId** (*string*) - This is a reference to the room the player currently occupies. Set this to the **ID** of the room the player should start in.
@@ -440,9 +440,15 @@ const demoDisk = {
**roomId** (*string*) - The unique identifier for the room.`
},
+ {
+ option: `Tell me about **GETITEM**`,
+ line: `getItem
is a function you can use to get a reference to an item in the player's inventory or in the current room. It takes one argument:
+
+ **name** (*string*) - The name of the item.`
+ },
{
option: `Tell me about **GETITEMINROOM**`,
- line: `getItemInRoom
is a function you can use to get a reference to an item in a particular room. It takes two arguments:
+ line: `getItemInRoom
is a function you can use to get a reference to an item in any room. It takes two arguments:
**itemName** (*string*) - The name of the item.
@@ -461,7 +467,7 @@ const demoDisk = {
],
},
],
-};
+});
// custom functions used by this disk
// change the CSS stylesheet to the one with the passed name
diff --git a/game-disks/new-disk-template.js b/game-disks/new-disk-template.js
index 4e5282c..1d4b137 100644
--- a/game-disks/new-disk-template.js
+++ b/game-disks/new-disk-template.js
@@ -1,6 +1,6 @@
// This simple game disk can be used as a starting point to create a new adventure.
// Change anything you want, add new rooms, etc.
-const newDiskTemplate = {
+const newDiskTemplate = () => ({
roomId: 'start', // Set this to the ID of the room you want the player to start in.
rooms: [
{
@@ -21,7 +21,7 @@ const newDiskTemplate = {
name: 'axe',
desc: `You could probably USE it to cut the VINES, unblocking the door.`,
isTakeable: true, // Allows the player to take the item.
- onUse: () => {
+ onUse() {
// Remove the block on the room's only exit.
const room = getRoom('start');
const exit = getExit('north', room.exits);
@@ -29,6 +29,9 @@ const newDiskTemplate = {
if (exit.block) {
delete exit.block;
println(`You cut through the vines, unblocking the door to the NORTH.`);
+
+ // Update the axe's description.
+ getItem('axe').desc = `You USED it to cut the VINES, unblocking the door.`;
} else {
println(`There is nothing to use the axe on.`);
}
@@ -55,4 +58,4 @@ const newDiskTemplate = {
],
}
],
-};
+});
diff --git a/game-disks/screen-creatures.js b/game-disks/screen-creatures.js
deleted file mode 100644
index 70936df..0000000
--- a/game-disks/screen-creatures.js
+++ /dev/null
@@ -1,79 +0,0 @@
-// a game by okaybenji & 23dogsinatrenchcoat
-// inspired by the famicase of the same name: http://famicase.com/20/softs/080.html
-
-const $ = query => document.querySelector(query);
-const $$ = query => document.querySelectorAll(query);
-const randomIntBetween = (min, max) => Math.floor(Math.random() * (max - min)) + min;
-const randomColor = () => {
- const minBrightness = 33;
- const maxBrightness = 67;
- const minSaturation = 75;
-
- let color = 'hsl(';
- color += randomIntBetween(0, 360) + ',';
- color += randomIntBetween(minSaturation, 100) + '%,';
- color += randomIntBetween(minBrightness, maxBrightness) + '%)';
- return color;
-};
-
-const screenCreatures = {
- roomId: 'start',
- rooms: [
- {
- name: 'Living Room',
- id: 'start',
- img: `
-
-
- o o
- \\ |
- \\.|-.
- (\\| )
- .==================.
- | .--------------. |
- | |--.__.--.__.--| |
- | |--.__.--.__.--| |
- | |--.__.--.__.--| |
- | |--.__.--.__.--| |
- | |--.__.--.__.--| |
- | '--------------'o|
- | """""""" o|
- '=================='
- `,
- desc: `You feel the glow of the television washing over you. You haven't yet dared to look directly at the screen.`,
- items: [
- {
- name: ['tv', 'television', 'screen'],
- desc: [
- `You feel something tug at you from the inside of your chest as you look at the screen. You feel an invisible hand pass though your skin as if you were clay and wrap its cold fingers around your lungs. You can’t breathe.
-The screen is filled with so many colors, swirling around in maddening patterns with no rhyme or reason. With each second you stare, it becomes harder and harder to look away. You swear you can see another pair of eyes looking back at you.`,
- `The colors begin to pour out of the screen until they envelop you like a cocoon. You feel the colors press against you, wrapping themselves around you like a second skin. The colors are suddenly seared away by a blinding white light. Yet still you see those eyes, wide and unblinking, seared on to the back of your eyelids. The light dissipates, and the world begins to come back into focus.
-You dare not close your eyes, for you know that if you do, it will be staring back at you.`],
- look({getRoom}) {
- // Get the TV ASCII art.
- const tv = $('.img');
- // Reset the innerHTML so this will work each time the player looks at the TV.
- tv.innerHTML = getRoom('start').img;
- // Add the scanline class to each line on the TV screen.
- tv.innerHTML = tv.innerHTML.replaceAll('--.__.--.__.--', `--.__.--.__.--`);
- // Set each element of the screen to a random color.
- const scanlines = $$('.scanline')
- const colorElements = char => {
- scanlines.forEach(scanline => scanline.innerHTML = scanline.innerHTML.replaceAll(char, `${char}`));
- };
- ['-', '.', '_'].forEach(colorElements);
- $$('.randomColor').forEach(e => e.style = `color: ${randomColor()}`);
- // Oscillate the waves.
- $$('.randomColor').forEach(e => {
- if (e.innerText === '_') {
- e.classList.add('oscillateUp');
- } else if (e.innerText === '-') {
- e.classList.add('oscillateDown');
- }
- });
- },
- },
- ],
- },
- ],
-};
diff --git a/game-disks/unlimited-adventure.js b/game-disks/unlimited-adventure.js
index 9101419..d697dcf 100644
--- a/game-disks/unlimited-adventure.js
+++ b/game-disks/unlimited-adventure.js
@@ -15,7 +15,7 @@ commands[0].help = help;
// switch to the retro style
document.getElementById('styles').setAttribute('href', 'styles/retro.css');
-const unlimitedAdventure = {
+const unlimitedAdventure = () => ({
roomId: 'gameOver', // The room the player is currently in. Set this to the room you want the player to start in.
inventory: [], // You can add any items you want the player to start with here.
rooms: [
@@ -75,11 +75,11 @@ WWWWW/\\| / \\|'/\\|/"\\
`,
// This is just here as an example of how you can use the onEnter property.
// This gets called when the player enters the room.
- onEnter: ({disk, println, getRoom}) => {
+ onEnter({disk, println, getRoom}) {
console.log('Entered', disk.roomId); // Logs "Entered endOfTheWorld"
},
items: [
- { name: 'key', desc: 'It looks like a key.', isTakeable: true, use: ({disk, println, getRoom}) => {
+ { name: 'key', desc: 'It looks like a key.', isTakeable: true, onUse({disk, println, getRoom}) {
// This method gets run when the user types "use key".
const room = getRoom(disk.roomId);
const door = room.items.find(item => item.name === 'door');
@@ -91,7 +91,7 @@ WWWWW/\\| / \\|'/\\|/"\\
println('There\'s nothing to use the key on.');
}
}},
- { name: 'book', desc: 'It appears to contain some sort of encantation, or perhaps... code.', isTakeable: true, use: ({disk, println, getRoom}) => {
+ { name: 'book', desc: 'It appears to contain some sort of encantation, or perhaps... code.', isTakeable: true, onUse({disk, println, getRoom}) {
const room = getRoom(disk.roomId);
const door = room.items.find(item => item.name === 'door');
@@ -101,7 +101,7 @@ WWWWW/\\| / \\|'/\\|/"\\
}
println('A door has appeared from nothing! It seems to go nowhere...');
- room.items.push({ name: 'door', desc: 'It seems to go nowhere...', isOpen: false, use: ({disk, println, enterRoom}) => {
+ room.items.push({ name: 'door', desc: 'It seems to go nowhere...', isOpen: false, onUse({disk, println, enterRoom}) {
const door = room.items.find(item => item.name === 'door');
if (door.isOpen) {
enterRoom('gameReallyOver');
@@ -122,4 +122,4 @@ WWWWW/\\| / \\|'/\\|/"\\
`,
},
],
-};
+});
diff --git a/game-disks/ur-dead.js b/game-disks/ur-dead.js
index dfad8f8..ba8e1f0 100644
--- a/game-disks/ur-dead.js
+++ b/game-disks/ur-dead.js
@@ -1,14 +1,19 @@
// NOTE: This game is a work in progress!
-const urDead = {
+const urDead = () => ({
roomId: 'title',
todo: [{id: 0, desc: `Figure out where you are.`}],
inventory: [
- {name: 'compass', desc: `You'd held onto it as a keepsake, even though in life the busted thing didn't work at all. Weirdly enough, it seems to function fine down here.`},
+ {
+ name: 'compass',
+ desc: `You'd held onto it as a keepsake, even though in life the busted thing didn't work at all. Weirdly enough, it seems to function fine down here.`,
+ onLook: go,
+ onUse: go
+ },
{
name: ['to-do list', 'todo list'],
desc: `The list contains the following to-do items:`,
- onLook: () => {
+ onLook() {
// sort to-do list by done or not, then by id descending
const list = disk.todo
.sort((a, b) => {
@@ -21,7 +26,7 @@ const urDead = {
list.forEach(println);
},
- onUse: ({item, disk}) => {
+ onUse({item, disk}) {
// Using the list is the same as looking at it.
println(item.desc);
item.onLook({disk});
@@ -47,7 +52,7 @@ const urDead = {
|| ||
==' '==
`,
- onEnter: () => {
+ onEnter() {
println('ur dead', 'title');
enterRoom('court');
},
@@ -55,13 +60,13 @@ const urDead = {
{
name: '🏀 Craggy Half-Court',
id: 'court',
- onEnter: () => {
+ onEnter() {
const room = getRoom('court');
if (room.visits === 1) {
println(`The bad news is you are dead. The good news is, it's not so bad. Type LOOK to have a look around.`);
- room.desc = `You see a couple of skeletons playing HORSE. A gate leads NORTH.`;
+ room.desc = `You see a couple of skeletons playing H.O.R.S.E. A gate leads NORTH.`;
}
},
items: [
@@ -83,7 +88,7 @@ const urDead = {
name: ['basketball', 'ball'],
desc: 'You could really have a ball with that thing.',
isTakeable: true,
- onTake: ({item, room}) => {
+ onTake({item, room}) {
const skeletons = getCharacter('dirk');
skeletons.topics = skeletons.tookBallTopics;
@@ -100,7 +105,7 @@ const urDead = {
room.desc = `You see a couple of skeletons. You get the feeling they don't care for you.`;
println(`One of the skeletons performs an elaborate dance to set up their shot, dribbling out a steady beat. They are clearly banking on the other forgetting one of the many the steps, and thus adding an 'O' to their 'H'. They're so swept up in their routine that you're able to step in and swipe the ball on a down beat.
- The skeletons don't look happy. (Later, you will confoundedly try to remember how you could TELL they looked uphappy.)`);
+ The skeletons don't look happy. (Later, you will confoundedly try to remember how you could *tell* they looked unhappy.)`);
item.onUse = () => println(`It's a bit hard to dribble on the uneven floor, but you manage to do so awkwardly.`);
}
@@ -113,7 +118,7 @@ const urDead = {
{
name: '🏖 The "Beach"',
id: 'beach',
- desc: `There's a sign that reads DEATH'S A BEACH. There's sand, to be sure, but there's no water in sight. And the sky is a pitch-black void.
+ desc: `There's a sign that reads *DEATH'S A BEACH*. There's sand, to be sure, but there's no water in sight. And the sky is a pitch-black void.
To the NORTH you see a yacht in the sand, lit up like a Christmas tree. You hear the bassy thumping of dance music.
@@ -121,7 +126,7 @@ To the SOUTH is the gate to the half-court.
There's a bearded skeleton by the sign. He seems to want to TALK.`,
items: [
- {name: 'sign', desc: `It says: DEATH'S A BEACH.`},
+ {name: 'sign', desc: `It says: *DEATH'S A BEACH*.`},
{name: 'yacht', desc: `You can't see it too clearly from here. You'll need to go further NORTH.`},
{name: 'sand', desc: `Just regular old beach sand.`, block: `You try to take it, but it slips through your fingers.`},
{name: 'no water', desc: `Didn't I say there wasn't any water?`},
@@ -149,7 +154,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
{
name: 'ramp',
desc: `Nothing's stopping you from having a good time but you. Type USE RAMP.`,
- onUse: () => {
+ onUse() {
enterRoom('deck');
// add exit after player has learned the USE command
@@ -205,11 +210,13 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
return;
}
- println(`Suddenly the lights turn on. A clerk with a colorful mohawk opens the door, says "Come on in," and heads back inside without waiting for you to comply.`)
+ println(`Suddenly the lights turn on. Wow! What a difference.
+
+ A clerk with a colorful mohawk opens the door, says "Come on in," and heads back inside without waiting for you to comply.`)
delete exit.block;
- getItemInRoom('store', 'parkingLot').desc = `It's open for business. Let's make it a Blockbuster night.`;
+ getItem('store').desc = `It's open for business. Let's make it a Blockbuster night.`;
},
},
{
@@ -238,7 +245,13 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
},
items: [
{name: ['shelf', 'shelves'], desc: `There are surprisingly few movie cases on the shelves. Actually, there are just three.`},
- {name: ['movies', 'cases'], desc: `The only titles they seem to have are *Toxic Avenger*, *The Bodyguard* and *Purple Rain*.`},
+ {
+ name: ['movies', 'cases', 'toxic avenger', 'the bodyguard', 'purple rain'],
+ desc: `The only titles they seem to have are *Toxic Avenger*, *The Bodyguard* and *Purple Rain*.`,
+ onTake() {
+ println(`Each time you lean over to pick up a case, the clerk clears their throat loudly enough to stop you.`);
+ },
+ },
{name: ['tv', 'mallrats'], desc: `Kevin Smith is dressed like Batman.`},
],
exits: [
@@ -248,7 +261,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
{
name: '⚓️ Front Yard',
id: 'yard',
- onEnter () {
+ onEnter() {
const room = getRoom('yard');
if (room.visits === 1) {
@@ -277,7 +290,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
isHidden: true,
onTake() {
// if the player doesn't have the key and the door is locked, show a message
- if (!getItemInInventory('key') && getItemInRoom('door', 'yard').isLocked) {
+ if (!getItem('key') && getItem('door').isLocked) {
println(`There's no key under it, if that's what you're thinking. This is its home. Better leave it be.`);
} else {
println(`This is its home. Better leave it be.`);
@@ -297,7 +310,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
onUse({item}) {
if (item.isLocked) {
// if the player has the key, unlock the door and enter the room
- const key = getItemInInventory('key');
+ const key = getItem('key');
if (key) {
key.onUse();
} else {
@@ -344,6 +357,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
{
name: ['TV', 'television', 'tube'],
desc: `It's a Zenith tube television with a dial and antenna.`,
+ onUse: () => useItem('vcr'),
},
{
name: 'VCR',
@@ -355,9 +369,9 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
'Romancing the Stone': () => {
println(`You hit PLAY and watch the cassette. You see trailers for *Rhinestone*, *Give My Regards to Broad Street*, and *Muppets Take Manhattan*, and finally, our feature presentation, *Romancing the Stone*. The movie is... fine.`);
- const videoCase = getItemInRoom('case', 'livingRoom');
+ const videoCase = getItem('case');
if (videoCase.wasSeen) {
- println(`You comply with the case's instructions to BE KIND and REWIND.`);
+ println(`You comply with the case's instructions to *BE KIND* and *REWIND*.`);
}
// eject the video
@@ -365,13 +379,16 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
println(`You eject the video, put it in its case, and add it to your INVENTORY.`);
disk.inventory.push({
name: [`*Romancing the Stone*`, 'video', 'vhs', 'tape'],
- desc: `It's in a case with the Blockbuster logo, the name of the film, and a BE KIND, REWIND sticker.`,
+ desc: `It's in a case with the Blockbuster logo, the name of the film, and a *BE KIND, REWIND* sticker.`,
});
// remove the video case from the room
const room = getRoom('livingRoom');
const caseIndex = room.items.findIndex(item => item === videoCase);
room.items.splice(caseIndex, 1);
+
+ // add a topic to the clerk convo at blockbuster
+ getCharacter('clerk').chatLog.push('gotStone');
},
'Purple Rain': () => {
// TODO!
@@ -393,9 +410,9 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
},
{
name: 'Blockbuster video case',
- desc: `The case is empty. Looks like it once held *Romancing the Stone*. A sticker says BE KIND, REWIND.`,
+ desc: `The case is empty. Looks like it once held *Romancing the Stone*. A sticker says *BE KIND, REWIND*.`,
onLook() {
- const videoCase = getItemInRoom('case', 'livingRoom');
+ const videoCase = getItem('case');
videoCase.wasSeen = true;
},
onTake() {
@@ -431,7 +448,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
},
{
option: 'GIVE the ball back',
- onSelected: () => {
+ onSelected() {
println(`Feeling a bit bad, you decide to return the ball and move on.`);
disk.methods.resetCourt();
if (disk.askedSkeletonNames) {
@@ -492,7 +509,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
name: 'key',
desc: `It's not a skeleton key, but it is a skeleton's key. Dirk's, to be specific.`,
onUse() {
- const door = getItemInRoom('door', 'yard');
+ const door = getItem('door');
if (disk.roomId === 'yard') {
delete door.isLocked;
println(`You use Dirk's key to open the door, placing it under the fake rock before entering into the living room.`);
@@ -500,7 +517,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
// leave the door unlocked
getRoom('yard').exits.push({dir: ['south', 'in', 'inside'], id: 'livingRoom'});
// remove the key from inventory
- const key = getItemInInventory('key');
+ const key = getItem('key');
const itemIndex = disk.inventory.findIndex(i => i === key);
disk.inventory.splice(itemIndex, 1);
} else {
@@ -531,7 +548,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
topics: [
{
option: `WHERE am I?`,
- line: `"This is the UNDERWORLD. Welcome!"`,
+ line: `"This is the ***underworld***. Welcome!"`,
removeOnRead: true,
onSelected: () => disk.methods.crossOff(0),
},
@@ -550,7 +567,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
"Well, one of them."
- You don't know how you can tell he is smiling, but you CAN tell.`,
+ You don't know how you can tell he is smiling, but you __can__ tell.`,
},
{
option: `Will I become a SKELETON, too?`,
@@ -574,19 +591,19 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
option: `HOW then?`,
prereqs: ['back'],
removeOnRead: true,
- line: `"To get out of here, and back up there," he points up, "you need two pieces of information. One, your NAME. And two, as you asked before, HOW you died."
+ line: `"To get out of here, and back up there," he points up, "you need two pieces of information. One, your __name__. And two, as you asked before, __how__ you died."
It's at this moment that you realize you don't know your own name.
He continues, "And there's a reason you're not likely to find this information: who would you ask? After all, we're all in the same boat, up the same river."
- He pauses to whistle a bar from COME SAIL AWAY.
+ He pauses to whistle a bar from *Come Sail Away*.
- "In other words, if I don't know so much as MY own name, how could I hope to tell you YOURS. You see?"
+ "In other words, if I don't know so much as *my* own name, how could I hope to tell you *yours*. You see?"
This talking, bearded skeleton is starting to make some sense.
- "Anyway, I wouldn't worry too much about it. If you do happen across your NAME and CAUSE OF DEATH, come back here and I'll tell you where to go and who to talk to about it. But that's a whole other story, and as I said, it's not likely to come up! Just make yourself at home and start getting used to the place."`,
+ "Anyway, I wouldn't worry too much about it. If you do happen across your __name__ and __cause of death__, come back here and I'll tell you where to go and who to talk to about it. But that's a whole other story, and as I said, it's not likely to come up! Just make yourself at home and start getting used to the place."`,
onSelected() {
// unlock asking Fran about her name
const fran = getCharacter('fran');
@@ -607,7 +624,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
"Anyhow, I've said my spiel. I'll be here if you need me."
You thank him and end the conversation.`,
- onSelected: () => {
+ onSelected() {
endConversation();
const skeleton = getCharacter('dave');
@@ -626,7 +643,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
{
option: `WHAT was I supposed to be doing again?`,
prereqs: ['home'],
- line: `"If you're still trying to get out of here, come back once you've learned your NAME and HOW you died. Otherwise, just check things out and try to have a nice time."`,
+ line: `"If you're still trying to get out of here, come back once you've learned your __name__ and __how you died__. Otherwise, just check things out and try to have a nice time."`,
onSelected: endConversation,
},
{
@@ -634,7 +651,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
prereqs: ['fran'],
removeOnRead: true,
line: () => `Oh, I'm Dave. Pleasure to make your acquaintance${disk.playerName ? ', ' + disk.playerName : ''}.`,
- onSelected: () => {
+ onSelected() {
// now that we know his name, let's call him by it
const dave = getCharacter('dave');
dave.name = ['Dave', 'bearded skeleton'];
@@ -681,7 +698,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
option: `WHO are you?`,
line: `"I'm Fran. Didn't you see the nametag?"`,
removeOnRead: true,
- onSelected: () => {
+ onSelected() {
// now that we know her name, let's call her by it
const fran = getCharacter('fran');
fran.name = ['Fran', 'skeleton in a red dress'];
@@ -827,176 +844,27 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
option: `Can't you just WAIVE the late fee?`,
line: `"Look, I don't know if you've noticed," they begin with a serious expression, "But our selection is a little lacking these days.
"We don't get new movies in, so when a customer doesn't bring one back, that's one less film on the shelves.
- "All that to say, I'll be happy to waive your late fee — *if* you bring back *Romancing the Stone*."`,
+ "That said, I'll be happy to waive your late fee — *if* you bring back *Romancing the Stone*."`,
prereqs: ['fee'],
onSelected() {
disk.todo.push({id: 4, desc: `Return *Romancing the Stone*.`})
},
},
+ {
+ option: `I'm here to RETURN *Romancing the Stone*`,
+ line: `"That's great! Unfortunately this is all Benji has written of the game you are playing. He's breaking the fourth wall to tell you that, through me. Bug him on Twitter to get him to write more. (Be sure to use the EXPORT command to save your game in the meantime.)"`,
+ prereqs: ['gotStone'],
+ }
],
},
],
methods: {
- utils: {
- // saves text from memory to disk
- saveFile: (content, filename) => {
- const a = document.createElement('a');
- const file = new Blob([content], {type: 'text/plain'});
-
- a.href = URL.createObjectURL(file);
- a.download = filename;
- a.click();
-
- URL.revokeObjectURL(a.href);
- },
- // creates input element to open file prompt (allows user to load exported game from disk)
- openFile: () => {
- const input = document.createElement('input');
- input.setAttribute('type', 'file');
- input.click();
-
- return input;
- },
- // asserts the command is not save, load, import or export, nor blank (could use a better name...)
- isNotMeta: (cmd) => !cmd.toLowerCase().startsWith('save')
- && !cmd.toLowerCase().startsWith('load')
- && !cmd.toLowerCase().startsWith('export')
- && !cmd.toLowerCase().startsWith('import')
- && cmd !== '',
- // applies string representing an array of input strings (used for loading saved games)
- applyInputs(string) {
- let ins = [];
-
- // parse, filtering out the save/load commands & empty strings
- try {
- ins = JSON.parse(string).filter(disk.methods.utils.isNotMeta);
- } catch(err) {
- println(`An error occurred. See error console for more details.`);
- console.error(`An error occurred while attempting to parse text-engine inputs.
- Inputs: ${string}
- Error: ${err}`);
- return;
- }
-
- while (ins.length) {
- applyInput(ins.shift());
- }
- },
- },
- commands: {
- // override help to include import/export
- help() {
- const instructions = `The following commands are available:
- LOOK: 'look at key'
- TAKE: 'take book'
- GO: 'go north'
- USE: 'use door'
- TALK: 'talk to mary'
- ITEMS: list items in the room
- INV: list inventory items
- SAVE/LOAD: save current game, or load a saved game (in memory)
- IMPORT/EXPORT: save current game, or load a saved game (on disk)
- HELP: this help menu
- `;
- println(instructions);
- },
- // overridden save command stores player input history
- // (optionally accepts a name for the save)
- save: (name = 'save') => {
- localStorage.setItem(name, JSON.stringify(inputs));
- const line = name.length ? `Game saved as "${name}".` : `Game saved.`;
- println(line);
- },
- // overridden load command reapplies inputs from saved game
- // (optionally accepts a name for the save)
- load: (name = 'save') => {
- if (inputs.filter(disk.methods.utils.isNotMeta).length > 2) {
- println(`At present, you cannot load in the middle of the game. Please reload the browser, then run the **LOAD** command again.`);
- return;
- }
-
- let save = localStorage.getItem(name);
-
- if (!save) {
- println(`Save file not found.`);
- return;
- }
-
- disk.methods.utils.applyInputs(save);
-
- const line = name.length ? `Game "${name}" was loaded.` : `Game loaded.`;
- println(line);
- },
- // export current game to disk (optionally accepts a filename)
- export(name) {
- const filename = `${name.length ? name : 'urdead'}.txt`;
- disk.methods.utils.saveFile(JSON.stringify(inputs), filename);
- println(`Game exported to "${filename}".`);
- },
- // import a previously exported game from disk
- import() {
- if (inputs.filter(disk.methods.utils.isNotMeta).length > 2) {
- println(`At present, you cannot load in the middle of the game. Please reload the browser, then run the **IMPORT** command again.`);
- return;
- }
-
- const input = disk.methods.utils.openFile();
- input.onchange = () => {
- const fr = new FileReader();
- const file = input.files[0];
-
- // register file loaded callback
- fr.onload = () => {
- // load the game
- disk.methods.utils.applyInputs(fr.result);
- println(`Game "${file.name}" was loaded.`);
- input.remove();
- };
-
- // register error handling
- fr.onerror = (event) => {
- println(`An error occured loading ${file.name}. See console for more information.`);
- console.error(`Reader error: ${fr.error}
- Reader error event: ${event}`);
- input.remove();
- };
-
- // attempt to load the text from the selected file
- fr.readAsText(file);
- };
- },
- play: () => println(`You're already playing a game.`),
- // set player's name
- name: (arg) => {
- if (!arg.length) {
- println(`Type NAME followed by the name you wish to choose.`);
- return;
- }
-
- disk.playerName = (Array.isArray(arg) ? arg.join(' ') : arg).toUpperCase();
- const nametag = disk.inventory.find(i => i.name === 'nametag');
-
- if (!nametag) {
- println(`You don't have a nametag.`);
- return;
- }
-
- nametag.desc = `It says ${disk.playerName}.`;
-
- // update Fran's greeting
- const fran = getCharacter('fran');
- fran.onTalk = () => println(`"Hello there, ${disk.playerName}."`);
-
- // confirm the change
- println(`Your name is now ${disk.playerName}.`);
- },
- },
// cross an item off player's to-do list
- crossOff: (id) => {
+ crossOff(id) {
disk.todo.find(item => item.id === id).done = true;
},
// reset the state of the basketball court
- resetCourt: () => {
+ resetCourt() {
const skeletons = getCharacter('dirk');
skeletons.topics = `They look pretty busy.`;
@@ -1011,7 +879,7 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
disk.inventory.splice(itemIndex, 1);
},
// check the player's blockbuster membership card
- checkCard: () => {
+ checkCard() {
const numberWithCommas = num => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// late fees increase with each command; cents are randomized
@@ -1026,9 +894,56 @@ There's a bearded skeleton by the sign. He seems to want to TALK.`,
}
},
},
+});
+
+const customCommands = {
+ play: () => println(`You're already playing a game.`),
+ leave: () => go(),
+ // set player's name
+ name(arg) {
+ if (!arg.length) {
+ println(`Type NAME followed by the name you wish to choose.`);
+ return;
+ }
+
+ disk.playerName = (Array.isArray(arg) ? arg.join(' ') : arg).toUpperCase();
+ const nametag = disk.inventory.find(i => i.name === 'nametag');
+
+ if (!nametag) {
+ println(`You don't have a nametag.`);
+ return;
+ }
+
+ nametag.desc = `It says ${disk.playerName}.`;
+
+ // update Fran's greeting
+ const fran = getCharacter('fran');
+ fran.onTalk = () => println(`"Hello there, ${disk.playerName}."`);
+
+ // confirm the change
+ println(`Your name is now ${disk.playerName}.`);
+ },
+ pet(arg) {
+ if (!arg.length) {
+ println(`Pet?`);
+ } else if (arg === 'rock' && getItem('rock')) {
+ println(`I think he likes it.`);
+ } else {
+ println(`You can't pet that.`);
+ }
+ },
+ open(arg) {
+ if (!arg.length) {
+ println(`Sesame?`);
+ } else if (arg === 'door' && getItem('door')) {
+ useItem('door');
+ } else {
+ println(`You can't open that.`);
+ }
+ },
};
// override commands to include custom commands
-commands[0] = Object.assign(commands[0], urDead.methods.commands);
-commands[1] = Object.assign(commands[1], urDead.methods.commands);
-commands[2] = Object.assign(commands[2], {play: urDead.methods.commands.play, name: urDead.methods.commands.name});
+commands[0] = Object.assign(commands[0], customCommands);
+commands[1] = Object.assign(commands[1], customCommands);
+commands[2] = Object.assign(commands[2], customCommands);
diff --git a/index.js b/index.js
index 3eb3250..4ff880e 100644
--- a/index.js
+++ b/index.js
@@ -1,13 +1,17 @@
// global properties, assigned with let for easy overriding by the user
+let diskFactory;
let disk;
// store user input history
-let inputs = [''];
+let inputs = [];
let inputsPos = 0;
// define list style
let bullet = '•';
+// queue output for improved performance
+let printQueue = [];
+
// reference to the input element
let input = document.querySelector('#input');
@@ -71,36 +75,122 @@ let setup = () => {
});
};
-// convert the disk to JSON and store it
+// store player input history
// (optionally accepts a name for the save)
-let save = (name) => {
- const save = JSON.stringify(disk, (key, value) => typeof value === 'function' ? value.toString() : value);
- localStorage.setItem(name, save);
+let save = (name = 'save') => {
+ localStorage.setItem(name, JSON.stringify(inputs));
const line = name.length ? `Game saved as "${name}".` : `Game saved.`;
println(line);
};
-// restore the disk from storage
+// reapply inputs from saved game
// (optionally accepts a name for the save)
-let load = (name) => {
- const save = localStorage.getItem(name);
+let load = (name = 'save') => {
+ let save = localStorage.getItem(name);
if (!save) {
println(`Save file not found.`);
return;
}
- disk = JSON.parse(save, (key, value) => {
- try {
- return eval(value);
- } catch (error) {
- return value;
- }
- });
+ // if the disk provided is an object rather than a factory function, the game state must be reset by reloading
+ if (typeof diskFactory !== 'function' && inputs.length) {
+ println(`You cannot load this disk in the middle of the game. Please reload the browser, then run the **LOAD** command again.`);
+ return;
+ }
+
+ inputs = [];
+ inputsPos = 0;
+ loadDisk();
+
+ applyInputs(save);
const line = name.length ? `Game "${name}" was loaded.` : `Game loaded.`;
println(line);
- enterRoom(disk.roomId);
+};
+
+// export current game to disk (optionally accepts a filename)
+let exportSave = (name) => {
+ const filename = `${name.length ? name : 'text-engine-save'}.txt`;
+ saveFile(JSON.stringify(inputs), filename);
+ println(`Game exported to "${filename}".`);
+};
+
+// import a previously exported game from disk
+let importSave = () => {
+ // if the disk provided is an object rather than a factory function, the game state must be reset by reloading
+ if (typeof diskFactory !== 'function' && inputs.length) {
+ println(`You cannot load this disk in the middle of the game. Please reload the browser, then run the **LOAD** command again.`);
+ return;
+ }
+
+ const input = openFile();
+ input.onchange = () => {
+ const fr = new FileReader();
+ const file = input.files[0];
+
+ // register file loaded callback
+ fr.onload = () => {
+ // load the game
+ inputs = [];
+ inputsPos = 0;
+ loadDisk();
+ applyInputs(fr.result);
+ println(`Game "${file.name}" was loaded.`);
+ input.remove();
+ };
+
+ // register error handling
+ fr.onerror = (event) => {
+ println(`An error occured loading ${file.name}. See console for more information.`);
+ console.error(`Reader error: ${fr.error}
+ Reader error event: ${event}`);
+ input.remove();
+ };
+
+ // attempt to load the text from the selected file
+ fr.readAsText(file);
+ };
+};
+
+// saves text from memory to disk
+let saveFile = (content, filename) => {
+ const a = document.createElement('a');
+ const file = new Blob([content], {type: 'text/plain'});
+
+ a.href = URL.createObjectURL(file);
+ a.download = filename;
+ a.click();
+
+ URL.revokeObjectURL(a.href);
+};
+
+// creates input element to open file prompt (allows user to load exported game from disk)
+let openFile = () => {
+ const input = document.createElement('input');
+ input.setAttribute('type', 'file');
+ input.click();
+
+ return input;
+};
+
+// applies string representing an array of input strings (used for loading saved games)
+let applyInputs = (string) => {
+ let ins = [];
+
+ try {
+ ins = JSON.parse(string);
+ } catch(err) {
+ println(`An error occurred. See error console for more details.`);
+ console.error(`An error occurred while attempting to parse text-engine inputs.
+ Inputs: ${string}
+ Error: ${err}`);
+ return;
+ }
+
+ while (ins.length) {
+ applyInput(ins.shift());
+ }
};
// list player inventory
@@ -205,6 +295,19 @@ let getExit = (dir, exits) => exits.find(exit =>
: exit.dir === dir
);
+// shortcuts for cardinal directions
+// (allows player to type e.g. 'go n')
+let shortcuts = {
+ n: 'north',
+ s: 'south',
+ e: 'east',
+ w: 'west',
+ ne: 'northeast',
+ nw: 'northwest',
+ se: 'southeast',
+ sw: 'southwest',
+};
+
// go the passed direction
// string -> nothing
let goDir = (dir) => {
@@ -219,7 +322,12 @@ let goDir = (dir) => {
const nextRoom = getExit(dir, exits);
if (!nextRoom) {
- println(`There is no exit in that direction.`);
+ // check if the dir is a shortcut
+ if (shortcuts[dir]) {
+ goDir(shortcuts[dir]);
+ } else {
+ println(`There is no exit in that direction.`);
+ }
return;
}
@@ -232,6 +340,7 @@ let goDir = (dir) => {
};
// shortcuts for cardinal directions
+// (allows player to type just e.g. 'n')
let n = () => goDir('north');
let s = () => goDir('south');
let e = () => goDir('east');
@@ -515,15 +624,16 @@ let chars = () => {
// display help menu
let help = () => {
const instructions = `The following commands are available:
- LOOK: 'look at key'
- TAKE: 'take book'
- GO: 'go north'
- USE: 'use door'
- TALK: 'talk to mary'
- ITEMS: list items in the room
- INV: list inventory items
- SAVE: save the current game
- LOAD: load the last saved game
+ LOOK: 'look at key'
+ TAKE: 'take book'
+ GO: 'go north'
+ USE: 'use door'
+ TALK: 'talk to mary'
+ ITEMS: list items in the room
+ CHARS: list characters in the room
+ INV: list inventory items
+ SAVE/LOAD: save current game, or load a saved game (in memory)
+ IMPORT/EXPORT: save current game, or load a saved game (on disk)
HELP: this help menu
`;
println(instructions);
@@ -551,6 +661,7 @@ let commands = [
{
inv,
i: inv, // shortcut for inventory
+ inventory: inv,
look,
l: look, // shortcut for look
go,
@@ -569,11 +680,14 @@ let commands = [
items,
use,
chars,
+ characters: chars,
help,
say,
save,
load,
restore: load,
+ export: exportSave,
+ import: importSave,
},
// one argument (e.g. "go north", "take book")
{
@@ -588,6 +702,8 @@ let commands = [
restore: x => load(x),
x: x => lookAt([null, x]), // IF standard shortcut for look at
t: x => talkToOrAboutX('to', x), // IF standard shortcut for talk
+ export: exportSave,
+ import: importSave, // (ignores the argument)
},
// two+ arguments (e.g. "look at key", "talk to mary")
{
@@ -604,8 +720,14 @@ let commands = [
// process user input & update game state (bulk of the engine)
// accepts optional string input; otherwise grabs it from the input element
let applyInput = (input) => {
+ let isNotSaveLoad = (cmd) => !cmd.toLowerCase().startsWith('save')
+ && !cmd.toLowerCase().startsWith('load')
+ && !cmd.toLowerCase().startsWith('export')
+ && !cmd.toLowerCase().startsWith('import');
+
input = input || getInput();
inputs.push(input);
+ inputs = inputs.filter(isNotSaveLoad);
inputsPos = inputs.length;
println(`> ${input}`);
@@ -624,8 +746,10 @@ let applyInput = (input) => {
let args = val.split(' ')
- // remove articles (except for the say command, which prints back what the user said)
- if (args[0] !== 'say') {
+ // remove articles
+ // (except for the say command, which prints back what the user said)
+ // (and except for meta commands to allow save names such as 'a')
+ if (args[0] !== 'say' && isNotSaveLoad(args[0])) {
args = args.filter(arg => arg !== 'a' && arg !== 'an' && arg != 'the');
}
@@ -718,8 +842,10 @@ let println = (line, className) => {
str = str.replace('\n', '
');
}
- output.appendChild(newLine).innerHTML = str;
- window.scrollTo(0, document.body.scrollHeight);
+ newLine.innerHTML = str;
+
+ // push into the queue to print to the DOM
+ printQueue.push(newLine);
};
// predict what the user is trying to type
@@ -891,6 +1017,10 @@ let getItemInRoom = (itemName, roomId) => {
// string -> item
let getItemInInventory = (name) => disk.inventory.find(item => objectHasName(item, name));
+// get item by name
+// string -> item
+let getItem = (name) => getItemInInventory(name) || getItemInRoom(name, disk.roomId)
+
// retrieves a keyword from a topic
// topic -> string
let getKeywordFromTopic = (topic) => {
@@ -945,19 +1075,39 @@ let endConversation = () => {
// load the passed disk and start the game
// disk -> nothing
let loadDisk = (uninitializedDisk) => {
+ if (uninitializedDisk) {
+ diskFactory = uninitializedDisk;
+ // start listening for user input
+ setup();
+ }
+
// initialize the disk
- disk = init(uninitializedDisk);
+ // (although we expect the disk to be a factory function, we still support the old object format)
+ disk = init(typeof diskFactory === 'function' ? diskFactory() : diskFactory);
// start the game
enterRoom(disk.roomId);
- // start listening for user input
- setup();
-
// focus on the input
input.focus();
};
+// append any pending lines to the DOM each frame
+let print = () => {
+ if (printQueue.length) {
+ while (printQueue.length) {
+ output.appendChild(printQueue.shift());
+ }
+
+ // scroll to the most recent output at the bottom of the page
+ window.scrollTo(0, document.body.scrollHeight);
+ }
+
+ requestAnimationFrame(print);
+}
+
+requestAnimationFrame(print);
+
// npm support
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = loadDisk;
diff --git a/readme.md b/readme.md
index 0211389..cd0079a 100644
--- a/readme.md
+++ b/readme.md
@@ -19,7 +19,7 @@ Very little programming is required, but several JavaScript hooks are provided i
### How do I use it?
To create your own adventure, you can use one of the files in the [game-disks](https://github.com/okaybenji/text-engine/blob/master/game-disks) folder as a template. For example, take a look at [the disk called newDiskTemplate](https://github.com/okaybenji/text-engine/blob/master/game-disks/new-disk-template.js).
-Include your "game disk" (JSON data) in index.html and load it with `loadDisk(myGameData)`. (Look at [index.html](https://github.com/okaybenji/text-engine/blob/master/index.html) in the repo for an example.)
+Include your "game disk" (a function returning JSON data) in index.html and load it with `loadDisk(myGameData)`. (Look at [index.html](https://github.com/okaybenji/text-engine/blob/master/index.html) in the repo for an example.)
The end product will be your very own text adventure game, similar to [this one](http://okaybenji.github.io/text-engine). It's a good idea to give that game a try to get introduced to the engine.
@@ -27,10 +27,10 @@ The end product will be your very own text adventure game, similar to [this one]
`text-engine` uses a disk metaphor for the data which represents your game, like the floppy disks of yore.
-Including [index.js](https://github.com/okaybenji/text-engine/blob/master/index.js) from this repository in your [index.html](https://github.com/okaybenji/text-engine/blob/master/index.html) `