Skip to content

Instantly share code, notes, and snippets.

@mjhoy
Last active December 15, 2015 21:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mjhoy/5327510 to your computer and use it in GitHub Desktop.
Save mjhoy/5327510 to your computer and use it in GitHub Desktop.
Adventure viewer

A little previewer for a text-based adventure game.

It will render a directed graph for you, using d3.js and d3.layouts.force. The arrows (showing directed edges) were a little tricky. They are rendered given source and target coordinates with some basic trigonometry:

arrow.attr("transform", function(d) {
    var p1  = [d.source.x,d.source.y],
        p2  = [d.target.x,d.target.y],
        ang = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]),
        deg = ang * 180 / Math.PI,
        dis = -8,
        dx  = dis * Math.cos(ang),
        dy  = dis * Math.sin(ang),
        x   = d.target.x + dx,
        y   = d.target.y + dy,
        rotation = deg + 270;
    return " translate(" + x + "," + y + ") rotate(" + rotation + ")";
  });

It looks like there is a much easier way to do this.

The format for the text is:

Title

*ID: A subject message.
A longer description,
spanning multiple lines.
>TARGET_ID: Target message.
>TARGET_ID: Target message.

*ID: [...]

The example game used is below. Use the “Reset” link to edit it.

Directed layouts

*A1: You wake up.
You're in the public library.
There are notes scattered all over the table. Something about
force-directed layouts.
>A2: Drink some water.

*A2: You drink some water.
It has a funny, bubblegum taste.
>A3: Walk outside.
>A4: Pick out the nearest book.

*A3: You walk outside.
The grackles, red-wing blackbirds, starlings, and Canada geese
are making a racket. The sun hurts your eyes.
>A6: Attempt to cross traffic.

*A4: You pick up the nearest book.
It’s called, “The Everything Bicycle Book.”
>A5: Read page 55.
>A1: Fall asleep.

*A5: You turn to page 55.
There’s a section on recumbent bikes. You are so inspired,
you go out and buy one. You’re riding it around. The sun
hurts your eyes.
>A6: Attempt to cross traffic.

*A6: You attempt to cross traffic,
and are hit by a bus.
<!doctype html>
<meta charset='utf-8'>
<style>
#container { position: relative; }
#container,textarea {font-family: Menlo,"Courier New", monospace;font-size:15px;}
h2 { font-size: 12px;text-transform:uppercase;color:#aaa; }
h3 { font-size: 17px; }
#options {margin-top:1em;}
li { margin: 0.5em 0; color: #555; cursor:pointer; }
li:hover { color: #000; text-decoration: underline;}
line { stroke: #000; }
circle { fill: #000; cursor: pointer; }
circle.selected { fill: yellow; }
.arrow { fill: white; stroke: #000; }
#left { width: 460px; position: absolute; }
#right { width: 440px; left: 480px; position: absolute; }
#form { width: 960px; position: relative; z-index: 10;}
#form a { position: absolute; right: 0; font-size: 12px; color: #555; top: 10px;}
#form form { width: 420px; margin: 1em auto; display: none; }
#form.active form { display: block; }
#form.active a { display: none; }
textarea { width: 420px; height: 400px; font-size: 11px;}
</style>
<div id="container">
<div id="left">
</div>
<div id="right">
</div>
<div id="form">
<a href="#">Reset</a>
<form onSubmit="return getTextValue()">
<h2>Paste text and click submit:</h2>
<textarea id="userText"></textarea>
<input type="submit">
</form>
</div>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 480,
height = 500,
title = "Untitled";
function parse(text) {
function fail(ln) {
throw(new Error("Problem parsing on line number "+ln+1));
}
var currentNode,
nodes = [],
lines = text.split("\n"),
line, l, m, i;
for (i=0, l=lines.length; i<l; i++) {
line = lines[i];
if(m = line.match(/^\*(\w+\d):\s*(.*?)\*?\s*$/)) {
// new node.
if(currentNode) nodes.push(currentNode);
currentNode = { id: m[1], subject: m[2] }
} else if(m = line.match(/^>(\w+\d):\s*(.*?)\s*$/)) {
if(!currentNode) fail(i);
if(!currentNode.options) currentNode.options = [];
currentNode.options.push({ target: m[1], subject: m[2] })
}
else {
if(!currentNode) {
if(!nodes.length) {
if(line.match(/\w+.*/)) title = line;
} else {
fail(i);
}
}else {
if(!currentNode.msg) currentNode.msg = "";
if(nodes.length > 0 && nodes[nodes.length - 1].msg.match(/^\s*$/)) {
currentNode.msg += line + "<br><br>";
}else {
currentNode.msg += line + "\n";
}
}
}
}
if(currentNode) nodes.push(currentNode);
return nodes;
}
function generateLinks(nodes) {
var i, j, l_o, l=nodes.length, node,
source, target,
links = [],
objectMap = {};
for(i=0;i<l;i++) { objectMap[nodes[i].id]=i; }
for(i=0;i<l;i++) {
node = nodes[i];
if(node.options) {
for(j=0,l_o=node.options.length;j<l_o;j++) {
target = objectMap[node.options[j].target];
if(!target)console.log("Target undefined", node.options[j]);
links.push({
target: target,
source: objectMap[node.id]
});
}
}
}
return links;
}
function init(nodes, links) {
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.charge(-420)
.linkDistance(80)
.size([width, height])
.start();
d3.select('#form').attr("class", "");
d3.select("#right").append("h2").text(title);
var subject = d3.select('#right').append("h3")
.attr("id", "subject");
var narration = d3.select('#right').append("div")
.attr("id", "narration");
var options = d3.select('#right').append("div")
.attr("id", "options");
var svg = d3.select("#left").append("svg:svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link");
var arrow = svg.selectAll(".arrow")
.data(links)
.enter().append("path")
.attr("d", d3.svg.symbol().type("triangle-down"))
.attr("class", "arrow");
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("id", function(d) { return d.id; })
.attr("r", 5)
.call(force.drag)
.on("click", function(d) { choose(d.id); });
var name = svg.selectAll(".name")
.data(nodes)
.enter().append("text")
.attr("dx", "0.6em")
.attr("dy", "0.4em")
.text(function(d) { return d.id; });
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
arrow.attr("transform", function(d) {
var p1 = [d.source.x,d.source.y],
p2 = [d.target.x,d.target.y],
ang = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]),
deg = ang * 180 / Math.PI,
dis = -8,
dx = dis * Math.cos(ang),
dy = dis * Math.sin(ang),
x = d.target.x + dx,
y = d.target.y + dy,
rotation = deg + 270;
return " translate(" + x + "," + y + ") rotate(" + rotation + ")";
});
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
name.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
});
svg.selectAll(".node")
.attr("class", function(d) {
var k = "node";
if(d.selecteD) k+= " selected";
return k
});
function choose(nodeid) {
svg.selectAll(".node").attr("class", "node");
var node = svg.select("#"+nodeid)
.attr("class", "node selected");
var d = node.data()[0];
subject.html(d.subject);
narration.html(d.msg);
options.selectAll('li').remove();
if(d.options) {
for(i=0,l=d.options.length;i<l;i++) {
(function(i) {
options.append("li").html(d.options[i].subject)
.on("click", function() { choose(d.options[i].target); });
})(i);
}
}
}
choose(nodes[0].id);
}
function destroy () {
d3.selectAll("#left *").remove();
d3.selectAll("#right *").remove();
}
d3.select("#form a").on("click", function() {
destroy();
d3.select('#form').attr("class", "active");
});
function setText(text) {
var nodes = parse(text);
var links = generateLinks(nodes);
d3.select('#form').attr("class", "active");
d3.select("#userText").node().value = text;
init(nodes, links);
}
function getTextValue() {
try {
var text = d3.select("#userText").node().value;
setText(text);
} catch(e) {
alert("Whoops! Something went wrong with the parsing.", e.message);
return false;
}
return false;
}
d3.text("README.md", function(error, text) {
var extract = (/<pre>\n?((.|\n)*)\n<\/pre>/).exec(text)[1]
setText(extract);
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment