-
-
Save sma/6338d6f132e7a6cca946 to your computer and use it in GitHub Desktop.
interactive fiction construction set in one page of HTML
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<!-- Storybuilder Copyright 2012 by Stefan Matthias Aust. All rights reserved. Use under BSD license. --> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta name="apple-mobile-web-app-capable" content="yes"> | |
<title>Storybuilder</title> | |
<script src="http://code.jquery.com/jquery-1.8.0.min.js"></script> | |
<script src="https://raw.github.com/chjj/marked/1a2b02c6695527bd54c4fdd1bfa2f3a4dd64ffc8/lib/marked.js"></script> | |
<style> | |
xmp { display: none; } | |
body { margin: 20px; padding: 0; background: #EEE; color: #444; font: normal 15px/22px Georgia,serif; } | |
body { max-width: 480px; min-height: 480px; } | |
h1, h2, h3, h4 { margin: 10px 0; color: #666; } | |
h1 { font: bold 23px/22px Georgia,serif; } | |
h2 { font: bold 18px/22px Georgia,serif; } | |
h3 { font: bold 15px/22px Georgia,serif; } | |
p, ul, ol { margin:10px 0; } | |
code, pre { border: 1px solid #DDD; border-radius: 3px; padding:2px; background: #F0F0F0; color: #666; } | |
code { font: normal 14px/22px Menlo,monospace; } | |
small code { font-size: 11px; } | |
pre { line-height: 18px; padding: 9px; overflow: auto; } | |
pre > code { font: normal 12px/18px Menlo,monospace; border: 0; } | |
.tagline { margin:-10px 0 10px 0; font-size: 12px; font-style: italic; } | |
a { color: #36C; font-weight: bold; } | |
ul { list-style-type: square; } | |
ul.choices, ul.choices li { margin: 0; padding: 0; list-style: none; } | |
a.btn { | |
display: block; min-height:22px; margin: 10px 0; padding: 10px; | |
border: 1px solid #BBB; border-radius: 10px; background: #DDD; | |
text-decoration: none; color: #444; font-size: 14px; | |
} | |
a.btn:hover { background-color:#444; border-color:#444; color:#EEE; } | |
#dice { position: relative; width: 240px; height: 240px; } | |
#dice .die { | |
position: absolute; width: 48px; height: 48px; border: 1mm solid #840; border-radius: 10px; background: #A50; | |
font: normal 40px/50px Helvetica,sans-serif; color: #FFF; text-align: center; | |
} | |
</style> | |
<script> | |
var sections = {}; | |
var start; | |
var deadEnd = new Section("", "Sackgasse", "Hier geht es nicht weiter.\n\n> Neu beginnen *start*"); | |
function Section(id, title, text){ | |
this.id = id; | |
this.title = title; | |
this.text = text; | |
} | |
function addSection(section){ | |
if (!start) start = section.id; | |
sections[section.id.toLowerCase()] = section; | |
} | |
function addSectionsFromText(text){ | |
var lines = text.split(/\n/), i = 0, j, m, id, title; | |
while (i < lines.length){ | |
if (!/^=+$/.test(lines[i + 1])) { i++; continue; } | |
m = lines[i].match(/^(?:(.*?)\s*:\s*)?(.*)$/); | |
title = m[2]; id = m[1] || title; | |
for (j = i + 2; j < lines.length; j++){ | |
if (/^=+$/.test(lines[j + 1])) break; | |
} | |
text = lines.slice(i + 2, j).join("\n"); | |
addSection(new Section(id, title, text)); | |
i = j; | |
} | |
} | |
function findSection(id){ | |
if (!id || id === 'start') id = start; | |
return sections[id.toLowerCase()] || deadEnd; | |
} | |
function showSection(section){ | |
$("title").text(section.title); | |
$("#title").text(section.title); | |
var text = section.text; | |
text = executeCommands(text); | |
text = text.replace(/^>> +(.*) +\[(.*?) +([-+]?\d+)\] +\*([^*]+)\* +\*([^*]+)\*/mg, | |
"<ul class='choices'><li><a class='btn' href='#' data-roll='$2' data-target='$3' " + | |
"data-success='#$4' data-failure='#$5'>$1</a></li></ul>"); | |
text = text.replace(/^> +(.*) +\*([^*]+)\*/mg, | |
"<ul class='choices'><li><a class='btn' href='#$2'>$1</a></li></ul>"); | |
text = text.replace(/<\/ul>\s*<ul class='choices'>/g, "\n"); | |
text = text.replace(/^{([^}]+)}(.*)/mg, "<div class='$1'>$2</div>"); | |
$("#text").html(marked(text)); | |
window.scrollTo(0, 1); | |
} | |
function gotoSection(id, track){ | |
if (history && track){ | |
history.pushState(null, null, "#" + id); | |
} | |
showSection(findSection(id)); | |
} | |
var attributesSection = "character"; | |
var state = {}; | |
function getAttribute(name){ | |
name = name.toLowerCase(); | |
var re = /^\* +(.*) +([-+]?\d+)/mg, text = findSection(attributesSection).text, m; | |
while ((m = re.exec(text))){ | |
if (m[1].toLowerCase() === name){ | |
return m[2]; | |
} | |
} | |
return "0"; | |
} | |
function setAttribute(name, value){ | |
name = name.toLowerCase(); | |
var section = findSection(attributesSection); | |
section.text = section.text.replace(/^\* +(.*) +([-+]?\d+)/mg, function(m, n, v){ | |
return n.toLowerCase() === name ? m.replace(v, value) : m; | |
}); | |
} | |
function executeCommands(text){ | |
return text.replace(/(^\^|\?)\[(.*?)\] *(.*)$/mg, function(_, t, command, rest){ | |
if (t === '?'){ | |
return String(getAttribute(command)) + executeCommands(rest); | |
} | |
if (/^(.*?)\s*([-+:]=)\s*([-+]?\d+)/.test(command)){ | |
var name = RegExp.$1, op = RegExp.$2, val = RegExp.$3; | |
if (op === ":=") setAttribute(name, val); | |
if (op === "+=") setAttribute(name, Number(getAttribute(name)) + Number(val)); | |
if (op === "-=") setAttribute(name, Number(getAttribute(name)) - Number(val)); | |
} else if (/^setze\s+(.*)/i.test(command)){ | |
state[RegExp.$1.toLowerCase()] = true; | |
} else if (/^lösche\s+(.*)/i.test(command)){ | |
delete state[RegExp.$1.toLowerCase()]; | |
} else if (/^wenn\s+(.*)/i.test(command)){ | |
return executeCondition(RegExp.$1, rest); | |
} | |
return executeCommands(rest); | |
}); | |
} | |
function executeCondition(condition, rest){ | |
condition = condition.split(/\s+(und|oder)\s+/i); | |
var r = evaluate(condition[0]), i = 1; | |
while (i < condition.length){ | |
if (/und/i.test(condition[i])){ | |
r &= evaluate(condition[i + 1]); | |
i += 2; | |
} else if (/oder/i.test(condition[i])){ | |
r |= evaluate(condition[i + 1]); | |
i += 2; | |
} else { | |
break; | |
} | |
} | |
return r ? executeCommands(rest) : ""; | |
function evaluate(c){ | |
if (/nicht\s+(.*)/i.test(c)){ | |
return !evaluate(RegExp.$1); | |
} | |
c = c.split(/\s*(<=?|>=?|!?==?)\s*/); | |
if (c.length === 3){ | |
var a = state[c[0].toLowerCase()]; | |
var b = state[c[2].toLowerCase()]; | |
switch (c[1]){ | |
case '=': case '==': return a == b; | |
case '!=': case '!==': return a != b; | |
case '<': return a < b; | |
case '>': return a > b; | |
case '<=': return a <= b; | |
case '>=': return a >= b; | |
} | |
} | |
return state[c[0].toLowerCase()]; | |
} | |
} | |
function resolve(roll, target, success, failure) { | |
var result = -4, i, die, wdiv = rnd(80) + 80, hdiv = rnd(80) + 80, top, left; | |
var $dice = $("<div id='dice'/>").css({left: (Math.min($(window).width(), 520) - 240) / 2}); | |
for (i = 0; i < 4; i++){ | |
die = rnd(3); | |
result += die; | |
// randomize die location on screen | |
top = (i < 2 ? rnd(hdiv - 80) : hdiv + rnd(160 - hdiv)) + 35; | |
left = (i % 2 ? rnd(wdiv - 80) : wdiv + rnd(160 - wdiv)) + 35; | |
$dice.append($("<div class='die'>").css({ top: top, left: left, | |
"-webkit-transform": "translate(-25px,-25px) rotate(" + (rnd(91) - 45) + "deg)" | |
}).text(["-1", "0", "+1"][die])); | |
} | |
var skill = getAttribute(roll) || "0"; | |
var ok = result + Number(skill) >= Number(target); | |
$("#title").text(roll + skill + " gegen " + target); | |
$("#text").html("Die Würfel fallen...").append($dice).append("<a class='btn' data-close='true' href='" + | |
(ok ? success : failure) + "'>Eine <big>" + (result + Number(skill)) + "</big> ist " + | |
(ok ? "" : "nicht ") + "ausreichend.</a>"); | |
} | |
function rnd(n){ | |
return Math.floor(Math.random() * n); | |
} | |
$(function(){ | |
$("xmp").each(function(i, e){ | |
if (e.id || e.title){ | |
addSection(new Section(e.id || e.title, e.title || e.id, $(e).text())); | |
} else addSectionsFromText($(e).text()); | |
}).remove(); | |
$("body").append($("<h1 id='title'/><div id='text'>")); | |
$("a.btn").live("click", function(e){ | |
e.preventDefault(); | |
if (this.dataset.roll){ | |
return resolve(this.dataset.roll, this.dataset.target, this.dataset.success, this.dataset.failure); | |
} | |
gotoSection(this.href.split("#")[1], true); | |
}); | |
gotoSection(start); | |
// doesn't work on Firefox | |
window.addEventListener("popstate", function(e){ | |
gotoSection(location.hash.substr(1)); | |
}); | |
// hide address bar on iOS | |
window.addEventListener("load", function(){ | |
setTimeout(function(){ window.scrollTo(0, 1); }, 0); | |
}); | |
}); | |
</script> | |
</head> | |
<body> | |
<xmp> | |
1: Storybuilder | |
=============== | |
{tagline}von Stefan Matthias Aust | |
... ist ein Generator für nicht-lineare Geschichten, bei denen der Leser | |
wählen kann, welchem Weg er folgen möchte. | |
Das ganze ist als einzelne HTML-Seite realisiert, sodass man ausgehend von | |
dieser Vorlage aus eigene Geschichten erstellen kann. | |
> Wie funktioniert das? *2* | |
2: Datenformat | |
============== | |
Text wird in *Abschnitte* unterteilt, die über *Verweise* verknüpft werden. | |
## Abschnitte | |
Text muss in `xmp`-Tags eingeschlossen werden. Entweder steht alles innerhalb | |
eines Tags. Dann muss jeder Abschnitt mit einer mit `=` unterstrichenen | |
Überschrift beginnen. Die Überschrift besteht aus einem optionalen | |
*Identifier*, gefolgt von einem Doppelpunkt (`:`) und dem Titel des | |
Abschnitts. Fehlt der Identifier, ist der Titel auch der Identifier. Oder es | |
gibt einen `xmp`-Tag pro Abschnitt, wobei dann das `id`-Attribut des Tags der | |
Identifier ist und das `title`-Attribut der Titel. Eines kann fehlen, aber | |
nicht beide. | |
1: Am Anfang | |
============ | |
An Anfang war alles trist und leer. | |
Später | |
====== | |
Später war es dann bunt und laut. | |
Gemäß [Markdown](http://de.wikipedia.org/wiki/Markdown) werden Absätze durch | |
Leerzeilen getrennt. Hervorhebungen gehen mit `*...*` oder `**...**` und | |
derartige Darstellung erreicht man mit `` `...` ``. Code-Beispiele werden mit | |
4 Leerzeichen eingerückt. Aufzählungen beginnen mit `* ...` am Zeilenanfang. | |
![Markdown Logo](https://raw.github.com/dcurtis/markdown-mark/master/png/66x40-solid.png) | |
## Verweise | |
Jeder Abschnitt sollte wenigsten auf einem anderen Abschnitt verweisen. Dazu | |
schreibt man nach einem `>` am Zeilenanfang eine Beschreibung in eine Zeile, | |
die mit dem in `*` eingeschlossenen Identifier des Abschnitts enden muss. | |
> Zeit vergeht *Später* | |
> Zurück zum Anfang *1* | |
## Proben | |
Alternativ kann man eine Probe gegen ein *Attribut* definieren und auf zwei | |
Abschnitte verweisen. Abhängig von dem Ergebnis der Probe wird bei Erfolg zum | |
ersten und bei Misserfolg zum zweiten Abschnitt gewechselt. | |
Attributen werden in einem `character` bezeichneten Abschnitt ein Wert | |
zugeordnet. Zur Zeit werden 4F (Fate Dice) gewürfelt, der Wert addiert und mit | |
dem angegebenen Zielwert verglichen. Wird er mindestens erreicht, ist die | |
Probe erfolgreich. | |
>> Ork angreifen [Kämpfen +2] *3* *4* | |
> Siehe Beispiel *3* | |
Attribute kann man auch setzen, erhöhen oder reduzieren, indem man die | |
folgenden *Befehle* in einen Abschnitt einbaut: | |
^[Kämpfen += 1] | |
^[Feilschen -= 2] | |
^[Gold := 4] | |
Den Wert eines Attributes kann man mit `?[...]` in den Text einfügen. | |
Du besitzt ?[Gold] Goldstücke. | |
## Weitere Befehle | |
Das folgende ist noch experimentell. Man kann Marker setzen, löschen oder | |
abfragen. Diese stellen den internen Zustand der Geschichte dar. Basierend auf | |
einem oder mehreren Markern können dann Verweise oder Absätze in einem | |
Abschnitt ausgeblendet werden: | |
^[setze Schatz gefunden] | |
^[lösche Schatz gefunden] | |
^[wenn Schatz gefunden] > Öffne Truhe *5* | |
^[wenn nicht Schatz gefunden] > Untersuche Gegend *6* | |
Ein Befehl muss am Zeilenanfang stehen und mit `^[` beginnen. | |
Mit `setze` kann ein ein Marker (im Beispiel `Schatz gefunden`) gesetzt | |
werden, mit `lösche` wird er wieder gelöscht. Mit `wenn` kann geprüft werden, | |
ob er setzt ist. Nur dann wird der Text danach berücksichtigt. Marker können | |
mit `und` und `oder` verknüpft und mit `nicht` negiert werden. | |
Es wäre auch wünschenswert, Attribute bei `wenn` zu benutzen: | |
^[wenn Kämpfen >= 2] | |
So kann es aussehen, wenn man ein Schwert aufnehmen kann: | |
In einer dunklen Höhle | |
====================== | |
Du nimmst das Schwert. | |
^[setze Schwert] | |
^[Kämpfen := +2] | |
Charakter | |
========= | |
Du besitzt: | |
^[wenn Schwert] * Ein schartiges Schwert | |
^[wenn Axt] * Eine alte Axt | |
Du hast ?[Gold] Gold. | |
Zur Zeit kann man bei `wenn` allerdings nur Marker und nicht Attribute abfragen. | |
> Ein Test *test* | |
<small>Hinweis: Möglicherweise ändere ich noch die Syntax von `[ ]` auf `( )`.</small> | |
</xmp> | |
<xmp id="3" title="Kampfbeispiel"> | |
Ein Kobold versperrt Dir den Weg. | |
>> Kobold angreifen [Kämpfen +2] *4* *5* | |
> Fliehen *6* | |
</xmp> | |
<xmp id="4" title="Gewonnen"> | |
Du hast überlebt. | |
^[Gold += 1] Du findest ein Goldstück. | |
> Zurück *3* | |
> Charakter *character* | |
</xmp> | |
<xmp id="5" title="Verloren"> | |
Du bist tot. | |
> Zurück *3* | |
</xmp> | |
<xmp id="character" title="Charakter"> | |
{tagline}Cormac O'Briain | |
Du bist ein erfahrener Krieger, der in einer Schlacht zu überleben weiß. | |
## Fertigkeiten | |
* Kämpfen +1 | |
* Feilschen -1 | |
## Vermögen | |
* Gold 0 | |
</xmp> | |
<xmp id="test"> | |
^[Kämpfen += 2] Kämpfen: ?[Kämpfen]. | |
^[Kämpfen -= 1] Kämpfen: ?[Kämpfen]. | |
^[Kämpfen := 0] Kämpfen: ?[Kämpfen]. | |
^[wenn Schwert] Ich habe ein Schwert. | |
^[setze Schwert] | |
^[wenn Schwert] Ich habe ein Schwert. | |
^[lösche Schwert] | |
^[wenn nicht Schwert] Ich habe kein Schwert. | |
^[wenn Schwert oder Axt] Du bist bewaffnet. | |
^[setze Axt] | |
^[wenn Schwert oder Axt] Du bist bewaffnet. | |
</xmp> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment