initial commit
This commit is contained in:
parent
9f71f899b5
commit
e5b9e87735
11 changed files with 637 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*.swp
|
||||||
|
bin/
|
||||||
|
node_modules/
|
72
Gruntfile.coffee
Normal file
72
Gruntfile.coffee
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
module.exports = (grunt) ->
|
||||||
|
pkg = require './package.json'
|
||||||
|
grunt.initConfig
|
||||||
|
pkg: pkg
|
||||||
|
|
||||||
|
coffee:
|
||||||
|
compile:
|
||||||
|
options:
|
||||||
|
join: true
|
||||||
|
bare: true
|
||||||
|
files: [
|
||||||
|
'bin/ficdown.js': ['src/*.coffee']
|
||||||
|
]
|
||||||
|
|
||||||
|
stylus:
|
||||||
|
compile:
|
||||||
|
options:
|
||||||
|
compress: true
|
||||||
|
expand: true
|
||||||
|
files: [
|
||||||
|
'bin/example/player.css': ['src/example/*.styl']
|
||||||
|
]
|
||||||
|
|
||||||
|
uglify:
|
||||||
|
js:
|
||||||
|
files:
|
||||||
|
'bin/ficdown.min.js': [
|
||||||
|
'bin/ficdown.js'
|
||||||
|
]
|
||||||
|
|
||||||
|
copy:
|
||||||
|
static:
|
||||||
|
files: [
|
||||||
|
expand: true
|
||||||
|
flatten: true
|
||||||
|
src: ['src/*.html']
|
||||||
|
dest: 'bin/'
|
||||||
|
]
|
||||||
|
example:
|
||||||
|
files: [
|
||||||
|
expand: true
|
||||||
|
flatten: true
|
||||||
|
src: ['src/example/*.html', 'src/example/*.png', 'src/example/*.md']
|
||||||
|
dest: 'bin/example/'
|
||||||
|
]
|
||||||
|
|
||||||
|
watch:
|
||||||
|
js:
|
||||||
|
files: ['src/**/*.coffee']
|
||||||
|
tasks: ['build:js']
|
||||||
|
css:
|
||||||
|
files: ['src/**/*.styl']
|
||||||
|
tasks: ['stylus:compile']
|
||||||
|
static:
|
||||||
|
files: ['src/**/*.html','src/**/*.js','src/**/*.md']
|
||||||
|
tasks: ['copy:static', 'copy:example']
|
||||||
|
|
||||||
|
for name of pkg.devDependencies when name.substring(0, 6) is 'grunt-'
|
||||||
|
grunt.loadNpmTasks name
|
||||||
|
|
||||||
|
grunt.registerTask 'build:js', [
|
||||||
|
'coffee:compile'
|
||||||
|
'uglify:js'
|
||||||
|
]
|
||||||
|
|
||||||
|
grunt.registerTask 'default', [
|
||||||
|
'coffee:compile'
|
||||||
|
'uglify:js'
|
||||||
|
'stylus:compile'
|
||||||
|
'copy:static'
|
||||||
|
'copy:example'
|
||||||
|
]
|
15
package.json
Normal file
15
package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "ficdown.js",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "A parser and player for Interactive Fiction written in Ficdown",
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"coffee-script": "^1.7.1",
|
||||||
|
"grunt": "^0.4.5",
|
||||||
|
"grunt-contrib-coffee": "^0.10.1",
|
||||||
|
"grunt-contrib-uglify": "^0.5.0",
|
||||||
|
"grunt-contrib-copy": "^0.5.0",
|
||||||
|
"grunt-contrib-watch": "^0.6.1",
|
||||||
|
"grunt-contrib-stylus": "^0.17.0"
|
||||||
|
}
|
||||||
|
}
|
26
src/example/index.html
Normal file
26
src/example/index.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>The Robot King</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link href="http://fonts.googleapis.com/css?family=Maven+Pro:400,700" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="player.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container" class="container">
|
||||||
|
<div id="main">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
|
||||||
|
<script src="http://cdnjs.cloudflare.com/ajax/libs/pagedown/1.0/Markdown.Converter.min.js"></script>
|
||||||
|
<script src="http://cdnjs.cloudflare.com/ajax/libs/pagedown/1.0/Markdown.Sanitizer.min.js"></script>
|
||||||
|
<script src="../ficdown.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$.get('story.md', function(data){
|
||||||
|
story = parseText(data);
|
||||||
|
player = new Player(story, 'main');
|
||||||
|
player.play();
|
||||||
|
}, 'text');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
39
src/example/player.styl
Normal file
39
src/example/player.styl
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
html, body
|
||||||
|
height 100%
|
||||||
|
body
|
||||||
|
font-family 'Maven Pro', sans-serif
|
||||||
|
background url('science.png') fixed repeat
|
||||||
|
html, body, h1, h2, p, ul, div
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
ul
|
||||||
|
list-style-type circle
|
||||||
|
margin-left 1.33em
|
||||||
|
#container
|
||||||
|
background-color rgba(255,255,255,0.95)
|
||||||
|
max-width 800px
|
||||||
|
margin 0 auto
|
||||||
|
height 100%
|
||||||
|
overflow-y scroll
|
||||||
|
overflow-x hidden
|
||||||
|
#main
|
||||||
|
position relative
|
||||||
|
padding 40px 60px
|
||||||
|
p, h1, h2
|
||||||
|
margin 1em 0
|
||||||
|
a
|
||||||
|
color #c00
|
||||||
|
text-decoration none
|
||||||
|
&.disabled
|
||||||
|
color #999
|
||||||
|
&.chosen
|
||||||
|
color #000
|
||||||
|
&:hover
|
||||||
|
color #000
|
||||||
|
&:hover
|
||||||
|
text-decoration underline
|
||||||
|
color #f00
|
||||||
|
&.disabled
|
||||||
|
text-decoration none
|
||||||
|
color #999
|
||||||
|
cursor not-allowed
|
BIN
src/example/science.png
Normal file
BIN
src/example/science.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 149 KiB |
178
src/example/story.md
Normal file
178
src/example/story.md
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
# [The Robot King](/robot-cave)
|
||||||
|
|
||||||
|
An experiment in markdown-based interactive fiction by Rudis Muiznieks.
|
||||||
|
|
||||||
|
This story was written in [Ficdown](http://rdsm.ca/ficdown), a subset of [Markdown](http://daringfireball.net/projects/markdown/) syntax with specific conventions for creating state-aware interactive fiction.
|
||||||
|
|
||||||
|
*Release r2014-07-01*
|
||||||
|
|
||||||
|
## Robot Cave
|
||||||
|
|
||||||
|
> You wake up and emit a great robot yawn as you stretch your metallic arms. Today is the big day! You have to start your new job working for the Robot King at the Robot Palace!
|
||||||
|
|
||||||
|
You're in your cave and you can see [a peg by the door where you usually hang your raincoat|your raincoat hanging by the door](?raincoat).
|
||||||
|
|
||||||
|
Your cave only has one tiny window, and through it you can see [the sun shining through the clouds|that it's raining heavily outside](?stopped-raining).
|
||||||
|
|
||||||
|
**What do you want to do?**
|
||||||
|
|
||||||
|
- [Go outside and start walking to the palace.](/outside)
|
||||||
|
- [|Wait for it to stop raining.](?stopped-raining#stopped-raining)
|
||||||
|
- [|Put on your raincoat.](?raincoat#raincoat)
|
||||||
|
|
||||||
|
### Raincoat
|
||||||
|
|
||||||
|
You take your raincoat and put it on. It fits perfectly!
|
||||||
|
|
||||||
|
### Stopped Raining
|
||||||
|
|
||||||
|
It feels like hours, but it finally stops raining. You hope you won't be late for your new job!
|
||||||
|
|
||||||
|
## Outside
|
||||||
|
|
||||||
|
> You step through the door and feel the water flowing over your metal body. Brrr! That's cold! You start to think that maybe getting your raincoat would be a good idea. This is just the kind of rain that might turn you into a rusty robot statue if you stay in it too long.
|
||||||
|
|
||||||
|
You're standing on your front porch in the pouring rain. You need to get to the palace for your new job, but you don't want to rust!
|
||||||
|
|
||||||
|
**What will you do?**
|
||||||
|
|
||||||
|
- [Continue walking to the palace.](/rusted)
|
||||||
|
- [Go back into your cave.](/robot-cave)
|
||||||
|
|
||||||
|
## [Outside](?stopped-raining)
|
||||||
|
|
||||||
|
You step through the door and feel the early afternoon sun warming your metal body. It feels good, but you were supposed to start your new job early in the morning!
|
||||||
|
|
||||||
|
You run as fast as you can all the way to the Robot Palace, but it's already too late.
|
||||||
|
|
||||||
|
"You were supposed to be here first thing in the morning," says the palace guard. "We can't have sleepy-head robots working at the Robot Palace! Try finding a different job instead."
|
||||||
|
|
||||||
|
**You've been fired!**
|
||||||
|
|
||||||
|
## Rusted!
|
||||||
|
|
||||||
|
You start walking toward the Robot Palace in the rain. Who needs a raincoat anyway? As you move down the path, rust starts forming on your legs and knees so you have to walk slower. Eventually the rust gets so bad that you can't move anymore at all!
|
||||||
|
|
||||||
|
As your whole body rusts over, you wonder what you could have been thinking. Only a crazy robot would ever go out into the rain without a raincoat!
|
||||||
|
|
||||||
|
You will have a long time to think about your mistake while you wait for another robot to come and help scrape off all the rust so you can move again. Since you never made it to the palace for your new job, you'll probably be fired.
|
||||||
|
|
||||||
|
**You have turned into a rusty robot statue!**
|
||||||
|
|
||||||
|
## [Outside](?raincoat)
|
||||||
|
|
||||||
|
You head out the door and into the rain. It's a good thing you put on your raincoat, because it's just the kind of rain that would probably turn you into a rusty robot statue if you stayed in it for too long.
|
||||||
|
|
||||||
|
You follow the road by your house all the way through town until you reach the door to the Robot Palace.
|
||||||
|
|
||||||
|
The palace guard looks you up and down. "What do you want?" he asks.
|
||||||
|
|
||||||
|
**What will you tell him?**
|
||||||
|
|
||||||
|
- ["I'm the new janitor-bot!"](/palace-gate#new-job)
|
||||||
|
- ["I'd like a tour of the palace!"](/palace-gate)
|
||||||
|
|
||||||
|
## Palace Gate
|
||||||
|
|
||||||
|
The robot guard looks at you and [nods|frowns](?new-job). "[Oh yeah, they told me to expect you. You're supposed to be starting today right?|We don't do tours on weekdays. Hey, aren't you the new janitor-bot who's starting today?](?new-job)"
|
||||||
|
|
||||||
|
**How will you answer?**
|
||||||
|
|
||||||
|
- ["Yup!"](/palace-entrance)
|
||||||
|
- ["Nope!"](/back-to-bed)
|
||||||
|
|
||||||
|
## Back to Bed
|
||||||
|
|
||||||
|
The robot guard looks at you with a confused expression on his face, then stops paying attention to you.
|
||||||
|
|
||||||
|
I guess you decided that you don't want a new job today after all. You turn around and walk all the way back home, where you hop back into bed for a quick nap.
|
||||||
|
|
||||||
|
Without a job, you fail to earn any money and you can no longer afford fuel to power yourself.
|
||||||
|
|
||||||
|
**You run out of fuel and shut down forever!**
|
||||||
|
|
||||||
|
## Palace Entrance
|
||||||
|
|
||||||
|
> The robot guard nods and ushers you into the palace through the large front doors.
|
||||||
|
|
||||||
|
> "You'll want to report to the Master Janitor Robot downstairs. He'll give you your uniform and get you started," the guard says, then quickly leaves and shuts the doors behind him.
|
||||||
|
|
||||||
|
The palace entrance is one of the biggest rooms you've ever seen! There are statues of knight-robots and pictures of all of the old Robot Kings going back for centuries lining the walls. The picture closest to you is a picture of the current Robot King. He looks a lot like you!
|
||||||
|
|
||||||
|
There is a grand double staircase leading up to the throne room, a hallway straight ahead that leads to the living quarters, and a door to your left that says "Stairs."
|
||||||
|
|
||||||
|
**Where do you want to go?**
|
||||||
|
|
||||||
|
- [Go upstairs to the throne room.](#throne-room)
|
||||||
|
- [Go through the hall to the living quarters.](/living-quarters)
|
||||||
|
- [Go downstairs to see the Master Janitor Robot.](/palace-basement)
|
||||||
|
|
||||||
|
### Throne Room
|
||||||
|
|
||||||
|
You start to ascend the stairs, but then think better of it. You wouldn't know what to do if you ran into the Robot King up there anyway!
|
||||||
|
|
||||||
|
## Living Quarters
|
||||||
|
|
||||||
|
You walk into the hall that leads to the living quarters, and find a gate blocking your way. There is a robot scanner installed on the gate. I guess it only opens for robots who live or work here. Maybe the Master Janitor Robot will have a way for you to get through.
|
||||||
|
|
||||||
|
[Go back to the palace entrance.](/palace-entrance#tried-gate)
|
||||||
|
|
||||||
|
## Palace Basement
|
||||||
|
|
||||||
|
> You walk down three flights of stairs until you reach the basement. The staircase is dark, but the basement is even darker. It's a little scary! You hope you can get the information you need from the Master Janitor Robot and get out of here as quickly as possible.
|
||||||
|
|
||||||
|
You're standing in the basement where new employees can pick up their uniforms and learn what their jobs are for the day.
|
||||||
|
|
||||||
|
[The Master Janitor Robot is pacing back and forth here, muttering to himself.|There is a funny looking robot here pacing back and forth, muttering to himself. That must be the Master Janitor Robot. When he notices you, he stops muttering and stares at you with crazy eyes.](?talked-to-master)
|
||||||
|
|
||||||
|
**What will you do?**
|
||||||
|
|
||||||
|
- [Go back upstairs.](/palace-entrance)
|
||||||
|
- [Ask the Master Janitor Robot what he's muttering about.](#talked-to-master+muttering)
|
||||||
|
- [|Ask the Master Janitor Robot about your uniform.](?uniform#talked-to-master+uniform)
|
||||||
|
- [Ask the Master Janitor Robot about the gate upstairs.](?tried-gate#talked-to-master+about-the-gate)
|
||||||
|
- [Ask the Master Janitor Robot about your job.](?uniform#started-job)
|
||||||
|
|
||||||
|
### Muttering
|
||||||
|
|
||||||
|
"Muttering?" says the Master Janitor Robot. "Was I muttering? I hadn't noticed."
|
||||||
|
|
||||||
|
The Master Janitor Robot pauses thoughtfully for a moment, then resumes muttering.
|
||||||
|
|
||||||
|
### Uniform
|
||||||
|
|
||||||
|
The Master Janitor Robot's eyes light up a pleasant shade of blue. "Ahh, you must be the new janitor-bot starting today!" he says.
|
||||||
|
|
||||||
|
He walks to a box in the corner and pulls out a blue janitor's uniform, then hands it to you. You put it on.
|
||||||
|
|
||||||
|
### About the Gate
|
||||||
|
|
||||||
|
"Ahh, yes, the gate," says the Master Janitor Robot. "Quite a clever contraption. There's a scanner attached that looks for a special device that's sewn into the [uniform I gave you|uniform that employees here wear](?uniform). [As I said, you'll want to head up there now to start cleaning room 13.](?started-job)"
|
||||||
|
|
||||||
|
### Started Job
|
||||||
|
|
||||||
|
["Like I said before, your|"Ready to get going?" says the Master Janitor Robot. He continues before you have a chance to answer. "Good, good. Your](?started-job) first job will be to clean room 13 in the living quarters. That's where the Robot King keeps all of his spare robes and crowns. There's a janitor's closet right next to that room where you can get a mop to clean the floors, and a duster to dust off the crowns."
|
||||||
|
|
||||||
|
The Master Janitor Robot scratches his chin for a moment, then resumes pacing back and forth and muttering to himself.
|
||||||
|
|
||||||
|
## [Living Quarters](?uniform)
|
||||||
|
|
||||||
|
You head into the hallway that leads to the living quarters and come to a large gate. A scanner attached to the gate lights up and beeps a few times. After a moment, you hear a click and a soft hiss as the gate opens to let you pass. Once you walk through, the gate hisses and clicks shut behind you.
|
||||||
|
|
||||||
|
You notice with some alarm that there's no scanner on the inside of the gate. You don't know how to get back out!
|
||||||
|
|
||||||
|
[Continue...](/living-quarters-2)
|
||||||
|
|
||||||
|
## [Living Quarters 2]("Living Quarters")
|
||||||
|
|
||||||
|
That's when you realize that you never asked the Master Janitor Bot what your job here was. You just took your uniform and left!
|
||||||
|
|
||||||
|
**You have failed to perform your new job because you never found out what it was.**
|
||||||
|
|
||||||
|
## [Living Quarters 2](?started-job "Living Quarters")
|
||||||
|
|
||||||
|
That's no problem though, because you already know what your job is. You continue down the hall, looking at and passing all of the doors until you come to the one marked with a "13." Right next to it is another door labeled "Janitor's Closet."
|
||||||
|
|
||||||
|
You open the closet and grab the mop and duster. You're so excited! Your first day as a janitor working for a Robot King that looks just like you, and you are about to enter a room containing all of his spare robes and crowns. What fun!
|
||||||
|
|
||||||
|
**You have reached the end of the intro to The Robot King.**
|
76
src/parser.coffee
Normal file
76
src/parser.coffee
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
parseText = (text) ->
|
||||||
|
lines = text.split /\n|\r\n/
|
||||||
|
blocks = extractBlocks lines
|
||||||
|
story = parseBlocks blocks
|
||||||
|
|
||||||
|
getBlockType = (hashCount) ->
|
||||||
|
switch hashCount
|
||||||
|
when 1 then 'story'
|
||||||
|
when 2 then 'scene'
|
||||||
|
when 3 then 'action'
|
||||||
|
|
||||||
|
extractBlocks = (lines) ->
|
||||||
|
blocks = []
|
||||||
|
currentBlock = null
|
||||||
|
for line in lines
|
||||||
|
match = line.match /^(#{1,3})\s+([^#].*)$/
|
||||||
|
if match?
|
||||||
|
if currentBlock != null
|
||||||
|
blocks.push currentBlock
|
||||||
|
currentBlock =
|
||||||
|
type: getBlockType match[1].length
|
||||||
|
name: match[2]
|
||||||
|
lines: []
|
||||||
|
else
|
||||||
|
currentBlock.lines.push line
|
||||||
|
if currentBlock != null
|
||||||
|
blocks.push currentBlock
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
parseBlocks = (blocks) ->
|
||||||
|
storyBlock = null
|
||||||
|
for block in blocks
|
||||||
|
if block.type == 'story'
|
||||||
|
storyBlock = block
|
||||||
|
break
|
||||||
|
storyName = matchAnchor storyBlock.name
|
||||||
|
storyHref = matchHref storyName.href
|
||||||
|
story =
|
||||||
|
name: storyName.text
|
||||||
|
description: trimText storyBlock.lines.join "\n"
|
||||||
|
firstScene: storyHref.target
|
||||||
|
scenes: {}
|
||||||
|
actions: {}
|
||||||
|
for block in blocks
|
||||||
|
switch block.type
|
||||||
|
when 'scene'
|
||||||
|
scene = blockToScene block
|
||||||
|
if !story.scenes[scene.key]
|
||||||
|
story.scenes[scene.key] = []
|
||||||
|
story.scenes[scene.key].push scene
|
||||||
|
when 'action'
|
||||||
|
action = blockToAction block
|
||||||
|
story.actions[action.state] = action
|
||||||
|
return story
|
||||||
|
|
||||||
|
blockToScene = (block) ->
|
||||||
|
sceneName = matchAnchor block.name
|
||||||
|
if sceneName?
|
||||||
|
title = if sceneName.title? then trimText sceneName.title else trimText sceneName.text
|
||||||
|
key = normalize sceneName.text
|
||||||
|
href = matchHref sceneName.href
|
||||||
|
if href?.conditions?
|
||||||
|
conditions = href.conditions.split '&'
|
||||||
|
else
|
||||||
|
title = trimText block.name
|
||||||
|
key = normalize block.name
|
||||||
|
scene =
|
||||||
|
name: title
|
||||||
|
key: key
|
||||||
|
description: trimText block.lines.join "\n"
|
||||||
|
conditions: if conditions? then conditions else null
|
||||||
|
|
||||||
|
blockToAction = (block) ->
|
||||||
|
action =
|
||||||
|
state: normalize block.name
|
||||||
|
description: trimText block.lines.join "\n"
|
108
src/player.coffee
Normal file
108
src/player.coffee
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
class Player
|
||||||
|
constructor: (@story, @id) ->
|
||||||
|
@converter = new Markdown.Converter()
|
||||||
|
@container = $("##{@id}")
|
||||||
|
@container.addClass('ficdown').data 'player', this
|
||||||
|
@playerState = {}
|
||||||
|
@visitedScenes = {}
|
||||||
|
@currentScene = null
|
||||||
|
@moveCounter = 0
|
||||||
|
|
||||||
|
play: ->
|
||||||
|
@container.html @converter.makeHtml "##{story.name}\n\n#{story.description}\n\n[Click to start...](/#{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 = 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: (scene, html) ->
|
||||||
|
temp = $('<div/>').append $.parseHTML html
|
||||||
|
if @visitedScenes[scene]
|
||||||
|
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 '## The End\n\nYou have reached the end of this story. <a id="restart" href="">Click here</a> to start over.'
|
||||||
|
$('#restart').click ->
|
||||||
|
player = new Player @story, @id
|
||||||
|
$("##{@id}").data 'player', player
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
handleHref: (href) ->
|
||||||
|
match = matchHref href
|
||||||
|
matchedScene = null
|
||||||
|
actions = []
|
||||||
|
if match?.toggles?
|
||||||
|
console.log "toggles: " + JSON.stringify 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 scene.conditions.length > matchedScene.conditions.length
|
||||||
|
matchedScene = scene
|
||||||
|
if matchedScene?
|
||||||
|
@currentScene = matchedScene
|
||||||
|
newScene = $.extend {}, @currentScene
|
||||||
|
newScene.description = @resolveDescription newScene.description
|
||||||
|
@disableOldLinks()
|
||||||
|
newContent = "###{newScene.name}\n\n"
|
||||||
|
newContent += "#{action.description}\n\n" for action in actions
|
||||||
|
newContent += newScene.description
|
||||||
|
newHtml = @processHtml newScene.key, @converter.makeHtml newContent
|
||||||
|
@visitedScenes[newScene.key] = true
|
||||||
|
scrollId = "move-#{@moveCounter++}"
|
||||||
|
@container.append $('<span/>').attr 'id', scrollId
|
||||||
|
@container.append newHtml
|
||||||
|
@wireLinks()
|
||||||
|
@checkGameOver()
|
||||||
|
console.log "scrolling to #{scrollId}"
|
||||||
|
@container.parent('.container').animate
|
||||||
|
scrollTop: $("##{scrollId}").offset().top - @container.offset().top
|
||||||
|
, 1000
|
48
src/tests.html
Normal file
48
src/tests.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<!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)
|
||||||
|
|
||||||
|
### 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</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);
|
||||||
|
}
|
||||||
|
doIt();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
72
src/util.coffee
Normal file
72
src/util.coffee
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
regexLib =
|
||||||
|
|
||||||
|
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])*)*))?$/
|
||||||
|
|
||||||
|
trim: /^\s+|\s+$/g
|
||||||
|
|
||||||
|
altText: /^((?:[^|\\]|\\.)*)(?:\|((?:[^|\\]|\\.)+))?$/
|
||||||
|
|
||||||
|
escapeChar: /\\(?=[^\\])/g
|
||||||
|
|
||||||
|
emptyListItem: /^\s*-\s*([\r\n]+|$)/gm
|
||||||
|
|
||||||
|
matchAnchor = (text) ->
|
||||||
|
re = new RegExp regexLib.anchors
|
||||||
|
match = re.exec text
|
||||||
|
if match?
|
||||||
|
result =
|
||||||
|
anchor: match[1]
|
||||||
|
text: match[2]
|
||||||
|
href: match[3]
|
||||||
|
title: match[6]
|
||||||
|
return result
|
||||||
|
return match
|
||||||
|
|
||||||
|
matchAnchors = (text) ->
|
||||||
|
re = new RegExp regexLib.anchors
|
||||||
|
anchors = []
|
||||||
|
while match = re.exec text
|
||||||
|
anchors.push
|
||||||
|
anchor: match[1]
|
||||||
|
text: match[2]
|
||||||
|
href: match[3]
|
||||||
|
title: match[6]
|
||||||
|
return anchors
|
||||||
|
|
||||||
|
trimText = (text) ->
|
||||||
|
text.replace regexLib.trim, ''
|
||||||
|
|
||||||
|
matchHref = (href) ->
|
||||||
|
re = new RegExp regexLib.href
|
||||||
|
match = re.exec href
|
||||||
|
if match?
|
||||||
|
result =
|
||||||
|
target: match[1]
|
||||||
|
conditions: match[2]
|
||||||
|
toggles: match[3]
|
||||||
|
return result
|
||||||
|
return match
|
||||||
|
|
||||||
|
normalize = (text) ->
|
||||||
|
text.toLowerCase().replace(/^\W+|\W+$/g, '').replace /\W+/g, '-'
|
||||||
|
|
||||||
|
conditionsMet = (state, conditions) ->
|
||||||
|
met = true
|
||||||
|
if conditions?
|
||||||
|
for condition in conditions
|
||||||
|
if !state[condition]?
|
||||||
|
met = false
|
||||||
|
break
|
||||||
|
return met
|
||||||
|
|
||||||
|
splitAltText = (text) ->
|
||||||
|
re = new RegExp regexLib.altText
|
||||||
|
match = re.exec text
|
||||||
|
if match?
|
||||||
|
result =
|
||||||
|
passed: match[1]
|
||||||
|
failed: match[2]
|
||||||
|
return result
|
||||||
|
return
|
Loading…
Reference in a new issue