Visualizing Storytelling (by tooling)
## Intro
I occasionally get excited to solve something originating from my freetime [game project](https://jakke.fi/blog/the-gift-roleplaying-game), as it presents quite a lot of different technical aspects and problems from my daily work in a very different programming sector and domain. Being passionate about programming, I enjoy solving these and find it refreshing :).
This post is about viewing, managing, and building capability for highly relational data. I'm not yet releasing source code for this fully, as the licensing of game project assets isn't final nor am I sure what my final goals are for game project either.
Mathematically, [Graph Theory](https://en.wikipedia.org/wiki/Graph_theory) is a good starting point for managing the data. For graph theory, there are numerous libraries that can visualize the data, I settled for using [reactflow.dev](https://reactflow.dev/).
## Source & Demo
Demo: [https://the-gift-sculptor.vercel.app/](https://the-gift-sculptor.vercel.app/)
Source: N/A

## Sculptor
I titled the project as __The Gift Sculptor__, as it sets to solve the problem of sculpting the world. The world happens to be heavily relative data that resides in Contentful which can be accessed using the GraphQL API, the tool is able to reuse some of the __lib__ capabilities built for the main game so it's mostly collection of the data into a Graph Theory fitting format and some views.
In the game for now, for the purpose of this visualization we can consider to have the following Models that we might want to inspect; Story, Character, Dialogue, Items, Encounter, Services (shop, inn), Monsters, Asset (sprite, music, voiceover).
Stories as an entity can be considered to be like pages of a fantasy book with a bit of digital flair, they can have references to other entities that can be accessed from this story. These are most often an encounter, a service, or some dialogue.

*Nested Encounter*
In this image, we have a story that has an encounter, which can be avoided by going to another story. If the player enters the encounter, it'll resolve into another encounter.
I'm building this tool to be able to create content for the game mostly without direct Contentful intervention. For now, it is only a readonly tool for inspection, but not too far from using a Contentful Management Token for managing the data.
In preparation for this, I've added Context Menu's for the nodes that can open more in depth views of the related data.

*Context Menu*

*Encounter Details*
## Depth-first search
Dfs isn't perhaps perfect for this job, but it'll do for now. In the game, we have a book entity which ties the whole game into a "book" that has a beginning. We can enter this __createStoryline__ function using that beginning as the story or something else, meaning that we can also filter out a specific section of the storyline for more in-depth inspection. Technically we might want to do this in the GraphQL, but we're working with a very small dataset for now.
```typescript
export async function createStoryline(story: ContentfulStory) {
const arc = await createStoryArc(story);
return getLinearStoryline(arc);
}
export function getLinearStoryline(storyArc: StoryArc): {
storyArcs: RecursiveStoryArc[];
} {
let linearStoryline: RecursiveStoryArc[] = [];
function dfs(storyArc: StoryArc, depth: number) {
if (storyArc.storyArcs) {
storyArc.storyArcs.sort((a, b) => {
return a.name.localeCompare(b.name);
});
for (let i = 0; i < storyArc.storyArcs.length; i++) {
dfs(storyArc.storyArcs[i], depth + 1);
}
}
linearStoryline.push({ ...storyArc, depth });
}
dfs({ ...storyArc }, 0);
return {
storyArcs: linearStoryline.reverse(),
};
}
```
## Graph Theory
The story in the game is for now fairly linear when inspected at large, but has some branching and different types of data that originate mostly to some story. I traversed the story data using [https://en.wikipedia.org/wiki/Depth-first_search](https://en.wikipedia.org/wiki/Depth-first_search) which could then be collected into different type of nodes and edges. These can then be represented by node type specific React components.
```typescript
function createGraphNodes(
story: RecursiveStoryArc,
index: number
): Node<any>[] {
const { id, name, ...rest } = story;
const position = { x: index * 400, y: story.depth * 400, z: 0 };
const nodes: Node<any>[] = [
{
id,
position,
data: { ...rest, link: createContentfulLink(id), name },
type: "storyFlowNode",
},
];
nodes.push(...createDialogueNodes(story, position));
nodes.push(...createMusicNodes(story, position));
nodes.push(...createServiceNodes(story, position));
nodes.push(...createEncounterNode(story, position));
nodes.push(...createVoiceOverNode(story, position));
return nodes;
}
function createEdges(story: RecursiveStoryArc): Edge[] {
const edges = story.storyArcs.map((arc) => ({
id: `${story.id}-${arc.id}`,
source: story.id,
target: arc.id,
animated: true,
}));
edges.push(...createEncounterEdges(story));
edges.push(...createDialogueEdges(story));
edges.push(...createMusicEdges(story));
edges.push(...createServiceEdges(story));
edges.push(...createVoiceOverEdges(story));
return edges;
}
```
The whole collection logic is fairly long, but here is the main gist of it.
## AudioFlowNode

e.g. this node is created for music and voiceovers and features a html5 audio element for listening the audio. Simple, but neat.
## Conclusion
There are a lot of possibilities in exploring data in this form and I learned a lot. [https://reactflow.dev](https://reactflow.dev) is a very powerful tool and I'm eager to explore it a bit more, for an instance the edges and positioning of the entities might require a bit more work and some algorithm. I'll revisit this probably.. until then 👋