Jakke.fi logo
jakke.fi/blog

Blogging mostly about my free-time projects.

HomeBlogvisualizing-storytelling-by-tooling

Visualizing Storytelling (by tooling)

@Type: Report

@Published:February 24, 2025 at 16:48

@Last Updated:February 24, 2025 at 17:27

@Author: Jakke Korpelainen

Intro

I occasionally get excited to solve something originating from my freetime game project, 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 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.

Source & Demo

Demo: https://the-gift-sculptor.vercel.app/

Source: N/A

sculptor-1

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.

sculptor-2 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.

sculptor4 Context Menu

sculptor5 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.

1export async function createStoryline(story: ContentfulStory) {
2  const arc = await createStoryArc(story);
3  return getLinearStoryline(arc);
4}
5
6export function getLinearStoryline(storyArc: StoryArc): {
7  storyArcs: RecursiveStoryArc[];
8} {
9  let linearStoryline: RecursiveStoryArc[] = [];
10
11  function dfs(storyArc: StoryArc, depth: number) {
12    if (storyArc.storyArcs) {
13      storyArc.storyArcs.sort((a, b) => {
14        return a.name.localeCompare(b.name);
15      });
16
17      for (let i = 0; i < storyArc.storyArcs.length; i++) {
18        dfs(storyArc.storyArcs[i], depth + 1);
19      }
20    }
21
22    linearStoryline.push({ ...storyArc, depth });
23  }
24
25  dfs({ ...storyArc }, 0);
26
27  return {
28    storyArcs: linearStoryline.reverse(),
29  };
30}
31

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 which could then be collected into different type of nodes and edges. These can then be represented by node type specific React components.

1function createGraphNodes(
2  story: RecursiveStoryArc,
3  index: number
4): Node<any>[] {
5  const { id, name, ...rest } = story;
6  const position = { x: index * 400, y: story.depth * 400, z: 0 };
7  const nodes: Node<any>[] = [
8    {
9      id,
10      position,
11      data: { ...rest, link: createContentfulLink(id), name },
12      type: "storyFlowNode",
13    },
14  ];
15
16  nodes.push(...createDialogueNodes(story, position));
17  nodes.push(...createMusicNodes(story, position));
18  nodes.push(...createServiceNodes(story, position));
19  nodes.push(...createEncounterNode(story, position));
20  nodes.push(...createVoiceOverNode(story, position));
21
22  return nodes;
23}
24
25function createEdges(story: RecursiveStoryArc): Edge[] {
26  const edges = story.storyArcs.map((arc) => ({
27    id: `${story.id}-${arc.id}`,
28    source: story.id,
29    target: arc.id,
30    animated: true,
31  }));
32
33  edges.push(...createEncounterEdges(story));
34  edges.push(...createDialogueEdges(story));
35  edges.push(...createMusicEdges(story));
36  edges.push(...createServiceEdges(story));
37  edges.push(...createVoiceOverEdges(story));
38
39  return edges;
40}
41

The whole collection logic is fairly long, but here is the main gist of it.

AudioFlowNode

sculptor3

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 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 👋