parent
fe8692bd05
commit
f105d9f1d9
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
@ -2,13 +2,24 @@
|
|||
"name": "ficdown.js",
|
||||
"version": "0.9.1",
|
||||
"description": "A parser and player for Interactive Fiction written in Ficdown",
|
||||
"scripts": {
|
||||
"build": "rm -rf ./build && tsc",
|
||||
"pack": "browserify build/unpacked/main.js --standalone Ficdown > ./build/ficdown.js",
|
||||
"minify": "uglifyjs build/ficdown.js > build/ficdown.min.js",
|
||||
"make": "npm run build && npm run pack && npm run minify"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.0.1",
|
||||
"jquery": "^3.4.0",
|
||||
"markdown-it": "^8.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/core-js": "^2.5.0",
|
||||
"@types/jquery": "^3.3.29",
|
||||
"@types/markdown-it": "0.0.7",
|
||||
"typescript": "^3.4.5"
|
||||
"@types/node": "^11.13.8",
|
||||
"browserify": "^16.2.3",
|
||||
"typescript": "^3.4.5",
|
||||
"uglify-js": "^3.5.9"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export type AltText = {
|
||||
passed: string,
|
||||
failed: string,
|
||||
passed?: string,
|
||||
failed?: string,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export class ParseError extends Error {
|
||||
constructor(message: string, public lineNumber: number) {
|
||||
super(message);
|
||||
super(lineNumber === 0
|
||||
? message
|
||||
: `[${ lineNumber }]: ${ message }`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export type PlayerOptions = {
|
||||
source: string,
|
||||
id: string,
|
||||
startText?: string,
|
||||
endText?: string,
|
||||
source: string, // ficdown story source
|
||||
id: string, // id of div to inject game into
|
||||
scroll?: boolean, // continuous scroll mode
|
||||
html?: boolean, // allow html in story source
|
||||
startText?: string, // custom link text to start game
|
||||
endMarkdown?: string, // custom markdown when game ends
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ export class Parser {
|
|||
|
||||
const story: Story = {
|
||||
name: storyName.text,
|
||||
description: Util.trimText(storyBlock.lines.join("\n")),
|
||||
description: Util.trimText(storyBlock.lines.map(l => l.text).join("\n")),
|
||||
firstScene: storyHref.target,
|
||||
scenes: {},
|
||||
actions: {},
|
||||
|
@ -102,7 +102,7 @@ export class Parser {
|
|||
name,
|
||||
key,
|
||||
conditions,
|
||||
description: Util.trimText(block.lines.join("\n")),
|
||||
description: Util.trimText(block.lines.map(l => l.text).join("\n")),
|
||||
lineNumber: block.lineNumber,
|
||||
};
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ export class Parser {
|
|||
private static blockToAction(block: Block): Action {
|
||||
return {
|
||||
state: Util.normalize(block.name),
|
||||
description: Util.trimText(block.lines.join("\n")),
|
||||
description: Util.trimText(block.lines.map(l => l.text).join("\n")),
|
||||
lineNumber: block.lineNumber,
|
||||
};
|
||||
}
|
||||
|
|
158
src/Player.ts
158
src/Player.ts
|
@ -1,24 +1,37 @@
|
|||
import {
|
||||
Action,
|
||||
BoolHash,
|
||||
PlayerOptions,
|
||||
Scene,
|
||||
Story,
|
||||
} from './Model';
|
||||
import { Parser } from './Parser';
|
||||
import { Util } from './Util';
|
||||
import * as Markdown from 'markdown-it';
|
||||
import * as $ from 'jquery';
|
||||
|
||||
export class Player {
|
||||
private static converter = new Markdown();
|
||||
private converter: Markdown;
|
||||
private container: JQuery<any>;
|
||||
private playerState: BoolHash = {};
|
||||
private visitedScenes: BoolHash = {};
|
||||
private currentScene?: Scene;
|
||||
private moveCounter: number = 0;
|
||||
private story: Story;
|
||||
private startText: string;
|
||||
private endMarkdown: string;
|
||||
|
||||
constructor(private options: PlayerOptions) {
|
||||
this.converter = new Markdown({
|
||||
html: options.html,
|
||||
});
|
||||
this.story = Parser.parse(options.source);
|
||||
this.startText = options.startText
|
||||
? options.startText
|
||||
: 'Click to start...';
|
||||
this.endMarkdown = options.endMarkdown
|
||||
? options.endMarkdown
|
||||
: "## The End\n\nYou have reached the end of this story. [Click here](restart()) to start over.";
|
||||
let i = 0;
|
||||
for(let [key, scenes] of Object.entries(this.story.scenes)) {
|
||||
for(let scene of scenes) scene.id = `#s${ ++i }`;
|
||||
|
@ -27,4 +40,147 @@ export class Player {
|
|||
this.container.addClass('ficdown').data('player', this);
|
||||
}
|
||||
|
||||
public play(): void {
|
||||
this.container.html(
|
||||
this.converter.render(`# ${ this.story.name }\n\n${ this.story.description }\n\n[${ this.startText }](/${ this.story.firstScene })`));
|
||||
this.wireLinks();
|
||||
}
|
||||
|
||||
public handleHref(href: string): false {
|
||||
const match = Util.matchHref(href);
|
||||
let matchedScene: Scene | undefined = undefined;
|
||||
const actions: Action[] = [];
|
||||
if(match && match.toggles) {
|
||||
const toggles = match.toggles.split('+');
|
||||
for(let toggle of toggles) {
|
||||
if(this.story.actions[toggle]) {
|
||||
const action: Action = { ...this.story.actions[toggle] };
|
||||
action.description = this.resolveDescription(action.description);
|
||||
actions.push(action);
|
||||
}
|
||||
this.playerState[toggle] = true;
|
||||
}
|
||||
}
|
||||
if(match && match.target) {
|
||||
if(this.story.scenes[match.target]) {
|
||||
for(let scene of this.story.scenes[match.target]) {
|
||||
if(Util.conditionsMet(this.playerState, scene.conditions)) {
|
||||
if(!matchedScene
|
||||
|| !scene.conditions
|
||||
|| !matchedScene.conditions
|
||||
|| Object.keys(scene.conditions).length > Object.keys(matchedScene.conditions).length) {
|
||||
matchedScene = scene;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(matchedScene) {
|
||||
this.currentScene = matchedScene;
|
||||
}
|
||||
const newScene: Scene = { ...this.currentScene! };
|
||||
newScene.description = this.resolveDescription(newScene.description);
|
||||
this.disableOldLinks();
|
||||
let newContent = newScene.name ? `## ${ newScene.name }\n\n` : '';
|
||||
for(let action of actions) {
|
||||
newContent += `${ action.description }\n\n`;
|
||||
}
|
||||
newContent += newScene.description;
|
||||
const newHtml = this.processHtml(newScene.id!, this.converter.render(newContent));
|
||||
this.visitedScenes[newScene.id!] = true;
|
||||
if(this.options.scroll) {
|
||||
const scrollId = `move-${ this.moveCounter++ }`;
|
||||
this.container.append($('<span/>').attr('id', scrollId));
|
||||
this.container.append(newHtml);
|
||||
$([document.documentElement, document.body]).animate({
|
||||
scrollTop: $(`#${ scrollId }`).offset()!.top,
|
||||
}, 1000);
|
||||
} else {
|
||||
this.container.html(newHtml);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
this.wireLinks();
|
||||
this.checkGameOver();
|
||||
return false;
|
||||
}
|
||||
|
||||
private resolveDescription(description: string): string {
|
||||
for(let anchor of Util.matchAnchors(description)) {
|
||||
const href = Util.matchHref(anchor.href);
|
||||
if(href && href.conditions) {
|
||||
const conditions = Util.toBoolHash(href.conditions.split('&'));
|
||||
const satisfied = Util.conditionsMet(this.playerState, conditions);
|
||||
const alts = Util.splitAltText(anchor.text);
|
||||
let replace = satisfied ? alts.passed : alts.failed;
|
||||
if(!replace) replace = '';
|
||||
replace = replace.replace(Util.regexLib.escapeChar(), '');
|
||||
if(replace === '' || (!href.toggles && !href.target)) {
|
||||
description = description.replace(anchor.anchor, replace);
|
||||
} else {
|
||||
let newHref = href.target ? `/${ href.target }` : '';
|
||||
newHref += href.toggles ? `#${ href.toggles }` : '';
|
||||
const newAnchor = `[${ replace }](${ newHref })`;
|
||||
description = description.replace(anchor.anchor, newAnchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
description = description.replace(Util.regexLib.emptyListItem(), '');
|
||||
return description;
|
||||
}
|
||||
|
||||
private disableOldLinks(): void {
|
||||
this.container.find('a:not(.external)').each((i, el) => {
|
||||
const $this = $(el);
|
||||
$this.addClass('disabled');
|
||||
$this.unbind('click');
|
||||
$this.click(() => false);
|
||||
});
|
||||
}
|
||||
|
||||
private processHtml(sceneId: string, html: string): string {
|
||||
const temp = $('<div/>').append($.parseHTML(html));
|
||||
if(this.visitedScenes[sceneId]) {
|
||||
temp.find('blockquote').remove();
|
||||
} else {
|
||||
temp.find('blockquote').each((i, el) => {
|
||||
const $this = $(el);
|
||||
$this.replaceWith($this.html());
|
||||
});
|
||||
}
|
||||
return temp.html();
|
||||
}
|
||||
|
||||
private wireLinks(): void {
|
||||
this.container.find('a:not(.disabled):not(.external)').each((i, el) => {
|
||||
const $this = $(el);
|
||||
const href = $this.attr('href');
|
||||
if(href) {
|
||||
if(!href.match(/^https?:\/\//)) {
|
||||
$this.click(() => {
|
||||
$this.addClass('chosen');
|
||||
const player: Player = $this.parents('.ficdown').data('player');
|
||||
return player.handleHref(href);
|
||||
});
|
||||
} else {
|
||||
$this.addClass('external');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkGameOver(): void {
|
||||
if(this.container.find('a:not(.disabled):not(.external)').length === 0) {
|
||||
this.container.append(this.converter.render(this.endMarkdown));
|
||||
const restartAnchor = this.container.find("a[href='restart()']");
|
||||
const options = this.options;
|
||||
restartAnchor.click(() => {
|
||||
const game = $(`#${ options.id }`);
|
||||
const player = new Player(options);
|
||||
game.empty();
|
||||
game.data('player', player);
|
||||
player.play();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
src/Util.ts
35
src/Util.ts
|
@ -1,18 +1,18 @@
|
|||
import { Anchor, Href, BoolHash, AltText } from './Model';
|
||||
|
||||
export class Util {
|
||||
private static regexLib: { [name: string]: RegExp } = {
|
||||
anchors: /(\[((?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[\])*\])*\])*\])*\])*\])*)\]\([ ]*((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\(\))*\))*\))*\))*\))*\))*)[ ]*((['"])(.*?)\5[ ]*)?\))/gm,
|
||||
public static regexLib: { [name: string]: () => RegExp } = {
|
||||
anchors: () => /(\[((?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[\])*\])*\])*\])*\])*\])*)\]\([ ]*((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\(\))*\))*\))*\))*\))*\))*)[ ]*((['"])(.*?)\5[ ]*)?\))/gm,
|
||||
|
||||
href: /^(?:\/([a-zA-Z](?:-?[a-zA-Z0-9])*))?(?:\?((?:!?[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:&!?[a-zA-Z](?:-?[a-zA-Z0-9])*)*)?)?(?:#((?:[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:\+[a-zA-Z](?:-?[a-zA-Z0-9])*)*))?$/,
|
||||
href: () => /^(?:\/([a-zA-Z](?:-?[a-zA-Z0-9])*))?(?:\?((?:!?[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:&!?[a-zA-Z](?:-?[a-zA-Z0-9])*)*)?)?(?:#((?:[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:\+[a-zA-Z](?:-?[a-zA-Z0-9])*)*))?$/,
|
||||
|
||||
trim: /^\s+|\s+$/g,
|
||||
trim: () => /^\s+|\s+$/g,
|
||||
|
||||
altText: /^((?:[^|\\]|\\.)*)(?:\|((?:[^|\\]|\\.)+))?$/,
|
||||
altText: () => /^((?:[^|\\]|\\.)*)(?:\|((?:[^|\\]|\\.)+))?$/,
|
||||
|
||||
escapeChar: /\\(?=[^\\])/g,
|
||||
escapeChar: () => /\\(?=[^\\])/g,
|
||||
|
||||
emptyListItem: /^[ ]*-\s*([\r\n]+|$)/gm,
|
||||
emptyListItem: () => /^[ ]*-\s*([\r\n]+|$)/gm,
|
||||
};
|
||||
|
||||
private static matchToAnchor(match: RegExpExecArray): Anchor {
|
||||
|
@ -30,21 +30,22 @@ export class Util {
|
|||
}
|
||||
|
||||
public static matchAnchor(text: string): Anchor | undefined {
|
||||
const match = this.regexLib.anchors.exec(text);
|
||||
const match = this.regexLib.anchors().exec(text);
|
||||
if(match) return this.matchToAnchor(match);
|
||||
}
|
||||
|
||||
public static matchAnchors(text: string): Anchor[] {
|
||||
let match: RegExpExecArray | null;
|
||||
const anchors: Anchor[] = [];
|
||||
while(match = this.regexLib.anchors.exec(text)) {
|
||||
const regex = this.regexLib.anchors();
|
||||
while((match = regex.exec(text)) !== null) {
|
||||
anchors.push(this.matchToAnchor(match));
|
||||
}
|
||||
return anchors;
|
||||
}
|
||||
|
||||
public static matchHref(text: string): Href | undefined {
|
||||
const match = this.regexLib.href.exec(text);
|
||||
const match = this.regexLib.href().exec(text);
|
||||
if(match) {
|
||||
return {
|
||||
target: match[1],
|
||||
|
@ -55,7 +56,7 @@ export class Util {
|
|||
}
|
||||
|
||||
public static trimText(text: string): string {
|
||||
return text.replace(this.regexLib.trim, '');
|
||||
return text.replace(this.regexLib.trim(), '');
|
||||
}
|
||||
|
||||
public static normalize(text: string): string {
|
||||
|
@ -76,7 +77,8 @@ export class Util {
|
|||
}
|
||||
}
|
||||
|
||||
public static conditionsMet(state: BoolHash, conditions: BoolHash): boolean {
|
||||
public static conditionsMet(state: BoolHash, conditions?: BoolHash): boolean {
|
||||
if(!conditions) return true;
|
||||
for(let [cond, val] of Object.entries(conditions)) {
|
||||
if((val && !state[cond]) || (!val && state[cond])) {
|
||||
return false;
|
||||
|
@ -85,13 +87,14 @@ export class Util {
|
|||
return true;
|
||||
}
|
||||
|
||||
public static splitAltText(text: string): AltText | undefined {
|
||||
const match = this.regexLib.altText.exec(text);
|
||||
public static splitAltText(text: string): AltText {
|
||||
const match = this.regexLib.altText().exec(text);
|
||||
if(match) {
|
||||
return {
|
||||
passed: match[0],
|
||||
failed: match[1],
|
||||
passed: match[1],
|
||||
failed: match[2],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
require('core-js');
|
||||
module.exports = require('./Player.js').Player;
|
|
@ -1,117 +0,0 @@
|
|||
class Player
|
||||
constructor: (
|
||||
@story,
|
||||
@id,
|
||||
@startText = "Click to start...",
|
||||
@endText = '## The End\n\nYou have reached the end of this story. <a id="restart" href="">Click here</a> to start over.'
|
||||
) ->
|
||||
@converter = new Markdown.Converter()
|
||||
@container = $("##{@id}")
|
||||
@container.addClass('ficdown').data 'player', this
|
||||
@playerState = {}
|
||||
@visitedScenes = {}
|
||||
@currentScene = null
|
||||
@moveCounter = 0
|
||||
i = 0
|
||||
scene.id = "s#{++i}" for scene in scenes for key, scenes of @story.scenes
|
||||
|
||||
play: ->
|
||||
@container.html @converter.makeHtml "##{@story.name}\n\n#{@story.description}\n\n[#{@startText}](/#{@story.firstScene})"
|
||||
@wireLinks()
|
||||
|
||||
wireLinks: ->
|
||||
@container.find('a:not(.disabled):not(.external)').each (i) ->
|
||||
$this = $(this)
|
||||
if !$this.attr('href').match(/^https?:\/\//)?
|
||||
$this.click ->
|
||||
$this.addClass 'chosen'
|
||||
player = $this.parents('.ficdown').data 'player'
|
||||
player.handleHref $this.attr 'href'
|
||||
return false
|
||||
else
|
||||
$this.addClass 'external'
|
||||
|
||||
resolveDescription: (description) ->
|
||||
for anchor in matchAnchors description
|
||||
href = matchHref anchor.href
|
||||
if href.conditions?
|
||||
conditions = toBoolHash href.conditions.split '&'
|
||||
satisfied = conditionsMet @playerState, conditions
|
||||
alts = splitAltText anchor.text
|
||||
replace = if satisfied then alts.passed else alts.failed
|
||||
if !replace?
|
||||
replace = ''
|
||||
replace = replace.replace regexLib.escapeChar, ''
|
||||
if replace == '' or (!href.toggles? and !href.target?)
|
||||
description = description.replace anchor.anchor, replace
|
||||
else
|
||||
newAnchor = "[#{replace}](#{ if href.target? then "/#{href.target}" else "" }#{ if href.toggles? then "##{href.toggles}" else "" })"
|
||||
description = description.replace anchor.anchor, newAnchor
|
||||
description = description.replace regexLib.emptyListItem, ''
|
||||
return description
|
||||
|
||||
disableOldLinks: ->
|
||||
@container.find('a:not(.external)').each (i) ->
|
||||
$this = $(this)
|
||||
$this.addClass 'disabled'
|
||||
$this.unbind 'click'
|
||||
$this.click -> return false
|
||||
|
||||
processHtml: (sceneid, html) ->
|
||||
temp = $('<div/>').append $.parseHTML html
|
||||
if @visitedScenes[sceneid]
|
||||
temp.find('blockquote').remove()
|
||||
else
|
||||
temp.find('blockquote').each (i) ->
|
||||
$this = $(this)
|
||||
$this.replaceWith $this.html()
|
||||
return temp.html()
|
||||
|
||||
checkGameOver: ->
|
||||
if @container.find('a:not(.disabled):not(.external)').length == 0
|
||||
@container.append @converter.makeHtml @endText
|
||||
$('#restart').data('info', [@id, @story]).click ->
|
||||
info = $(this).data 'info'
|
||||
$("##{info[0]}").empty()
|
||||
player = new Player info[1], info[0]
|
||||
$("##{info[0]}").data 'player', player
|
||||
player.play()
|
||||
return false
|
||||
|
||||
handleHref: (href) ->
|
||||
match = matchHref href
|
||||
matchedScene = null
|
||||
actions = []
|
||||
if match?.toggles?
|
||||
toggles = match.toggles.split '+'
|
||||
for toggle in toggles
|
||||
if @story.actions[toggle]?
|
||||
action = $.extend {}, @story.actions[toggle]
|
||||
action.description = @resolveDescription action.description
|
||||
actions.push action
|
||||
@playerState[toggle] = true
|
||||
if match?.target?
|
||||
if @story.scenes[match.target]?
|
||||
for scene in @story.scenes[match.target]
|
||||
if conditionsMet @playerState, scene.conditions
|
||||
if !matchedScene? or !scene.conditions? or !matchedScene.conditions? or Object.keys(scene.conditions).length > Object.keys(matchedScene.conditions).length
|
||||
matchedScene = scene
|
||||
if matchedScene?
|
||||
@currentScene = matchedScene
|
||||
newScene = $.extend {}, @currentScene
|
||||
newScene.description = @resolveDescription newScene.description
|
||||
@disableOldLinks()
|
||||
newContent = ""
|
||||
newContent += "###{newScene.name}\n\n" if newScene.name?
|
||||
newContent += "#{action.description}\n\n" for action in actions
|
||||
newContent += newScene.description
|
||||
newHtml = @processHtml newScene.id, @converter.makeHtml newContent
|
||||
@visitedScenes[newScene.id] = true
|
||||
scrollId = "move-#{@moveCounter++}"
|
||||
@container.append $('<span/>').attr 'id', scrollId
|
||||
@container.append newHtml
|
||||
@wireLinks()
|
||||
@checkGameOver()
|
||||
@container.parent('.container').animate
|
||||
scrollTop: $("##{scrollId}").offset().top - @container.offset().top
|
||||
, 1000
|
|
@ -1,52 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Ficdown.js Tests</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<textarea style="height:200px;width:600px;" id="input"># [Test Story](/test-scene)
|
||||
|
||||
This is a test story by Rudis Muiznieks
|
||||
|
||||
Version 1
|
||||
|
||||
## Test Scene
|
||||
|
||||
This is a test scene.scene
|
||||
|
||||
- [This is an option.](/test-scene-2)
|
||||
- [This is another option.](#test-condition)
|
||||
- [Another scene.](/test-scene-3)
|
||||
|
||||
### Test Condition
|
||||
|
||||
You have [|not](?test-condition) selected that option before.
|
||||
|
||||
## [Test Scene](?test-condition "Different Title")
|
||||
|
||||
This is the scene that matches a condition.
|
||||
|
||||
## Test Scene 2
|
||||
|
||||
This is a second scene
|
||||
|
||||
## [Test Scene 3]("")
|
||||
|
||||
This scene has no title.</textarea>
|
||||
<div><button onclick="doIt()">Do It</button></div>
|
||||
<pre id="output">
|
||||
|
||||
</pre>
|
||||
</div>
|
||||
<script src="ficdown.js"></script>
|
||||
<script>
|
||||
(function doIt(){
|
||||
var storyText = document.getElementById('input').value;
|
||||
var story = parseText(storyText);
|
||||
document.getElementById('output').innerHTML = JSON.stringify(story,null,2);
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>Ficdown.js Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<textarea id='source' style='display: none;'>
|
||||
# [Cloak of Darkness](/foyer)
|
||||
|
||||
A basic IF demonstration.
|
||||
|
||||
Hurrying through the rainswept November night, you're glad to see the bright lights of the Opera House. It's surprising that there aren't more people about but, hey, what do you expect in a cheap demo game...?
|
||||
|
||||
## [Foyer]("Foyer of the Opera House")
|
||||
|
||||
[You](#examined-self) are standing in a spacious hall, splendidly decorated in red and gold, with glittering chandeliers overhead. The entrance from the street is to the [north](#tried-to-leave), and there are doorways [south](/bar) and [west](/cloakroom).
|
||||
|
||||
### Tried to Leave
|
||||
|
||||
[You've](#examined-self) only just arrived, and besides, the weather outside seems to be getting worse.
|
||||
|
||||
### Examined Self
|
||||
|
||||
[You aren't carrying anything.|You are wearing a handsome cloak, of velvet trimmed with satin, and slightly splattered with raindrops. Its blackness is so deep that it almost seems to suck light from the room.](?lost-cloak)
|
||||
|
||||
## Cloakroom
|
||||
|
||||
The walls of this small room were clearly once lined with hooks, though now only one remains. The exit is a door to the [east](/foyer).
|
||||
|
||||
[Your cloak is on the floor here.](?dropped-cloak)
|
||||
[Your cloak is hanging on the hook.](?hung-cloak)
|
||||
|
||||
- [Examine the hook.](#examined-hook)
|
||||
- [Hang your cloak on the hook.](?examined-self&!lost-cloak#lost-cloak+hung-cloak)
|
||||
- [Drop your cloak on the floor.](?examined-self&!lost-cloak#lost-cloak+dropped-cloak)
|
||||
|
||||
### Examined Hook
|
||||
|
||||
It's just a small brass hook, [with your cloak hanging on it|screwed to the wall](?hung-cloak).
|
||||
|
||||
## [Bar]("Foyer Bar")
|
||||
|
||||
You walk to the bar, but it's so dark here you can't really make anything out. The foyer is back to the [north](/foyer).
|
||||
|
||||
- [Feel around for a light switch.](?!scuffled1#scuffled1+not-in-the-dark)
|
||||
- [Sit on a bar stool.](?!scuffled2#scuffled2+not-in-the-dark)
|
||||
|
||||
### Not in the Dark
|
||||
|
||||
In the dark? You could easily disturb something.
|
||||
|
||||
## [Bar](?lost-cloak "Foyer Bar")
|
||||
|
||||
The bar, much rougher than you'd have guessed after the opulence of the foyer to the north, is completely empty. There seems to be some sort of message scrawled in the sawdust on the floor. The foyer is back to the [north](/foyer).
|
||||
|
||||
[Examine the message.](/message)
|
||||
|
||||
## [Message]("Foyer Bar")
|
||||
|
||||
The message, neatly marked in the sawdust, reads...
|
||||
|
||||
**You have won!**
|
||||
|
||||
## [Message](?scuffled1&scuffled2 "Foyer Bar")
|
||||
|
||||
The message has been carelessly trampled, making it difficult to read. You can just distinguish the words...
|
||||
|
||||
**You have lost.**
|
||||
</textarea>
|
||||
<div id='game' />
|
||||
<script src='build/ficdown.min.js'></script>
|
||||
<script>
|
||||
var player = new Ficdown({
|
||||
id: 'game',
|
||||
source: document.getElementById('source').value,
|
||||
scroll: true,
|
||||
});
|
||||
player.play();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,14 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [ "es2017" ],
|
||||
"target": "es6",
|
||||
"target": "es5",
|
||||
"lib": ["es2017", "dom"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./build",
|
||||
"declaration": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"strict": true,
|
||||
"sourceMap": true
|
||||
"baseUrl": "./src",
|
||||
"outDir": "./build/unpacked",
|
||||
"allowJs": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue