D&D and Making Typst Figures

Author

Josef Fruehwald

Published

September 13, 2025

Doi

I’m currently running a D&D campaign, and in a recent session we ended with the plan for the party to interrogate a character they’d captured. We play every other week, which meant I had two whole weeks to figure out what exactly this character knew. I was worried about a few things though.

An interrogation where just one player with the highest Charisma asks all the questions, and the character resists (“No, no! I won’t tell you anything!”) so the player just keeps re-asking until they roll a high enough Intimidation check (“Ok! Fine! I give up! I’ll tell you everything!”) could be kind of boring. And I didn’t want the players to get bored and give up on the interrogation, because this character they captured actually had some pretty important information that could animate the party’s next steps. So I tried thought of ways I could structure the interrogation to have mechanics of its own, which led me to making a game board in Typst.

Typst

Typst is a new typesetting system that, imo, is way easier to use and customize than LaTeX (and there’s built in support in Quarto). There’s also a growing ecosystem of packages, including CeTZ (meant to the the Typst version of TiKZ) and fletcher which is built on top of CeTZ.

Embedding Typst Diagrams

While Quarto supports rendering entire documents to Typst, it doesn’t have the ability to run a Typst code chunk and embed the output in a different document format, like it can do with plotting R code chunks. For a separate project, I solved this by setting up a Makefile, and added running it to my pre-render script.

UNAME := $(shell uname)

ifeq ($(UNAME),Darwin)
	TYPST=~/.local/bin/typst
else
	TYPST=/usr/local/bin/typst
endif

srces := $(wildcard posts/20*/*/20*/typst/*.typ)
targets := $(patsubst %.typ,%.svg,$(srces))

all: $(srces) $(targets)

%.svg: %.typ
	$(TYPST) compile --font-path fonts -f svg $< $@

Now, every time I re-render the post, changes I make to the typst source files will be reflected in the embedded images.

First fletcher diagram

Here’s an annotated version of the typst file to generate an A->B->C->A diagram in typst using fletcher.

// package imports
#import "@preview/fletcher:0.5.8" as fletcher: (
  diagram, 
  node, 
  edge
)

// By default, the page size is A4.
// But since I'm making a standalone diagram,
// I want it to shrink automatically shrink
// to the size of the content.
#set page(
  width: auto,
  height: auto, 
  margin: 5mm, 
  fill: white
)

// This is a simple array of 
// node labels
#let nodes = ("A", "B", "C")

// This is an array of from-to edges
#let edges = (
  (0,1),
  (1,2),
  (2,0)
)

#diagram({
  for (i, n) in nodes.enumerate() {
    node(
      // Node coordinates
      (i, 0), 
      // node text
      n, 
      // node line
      stroke: 1pt, 
      // node id
      name: str(i)
    )
  }
  // argument unpacking
  for (from, to) in edges {
    // typed numeric variables
    let bend = 0deg
    // on the return arrow
    // arc large
    if(to < from){
      bend = 60deg
    }
    edge(
      label(str(from)), 
      label(str(to)), 
      // edge type
      "-|>", 
      bend: bend
    )
  }
})

// package imports
#import "@preview/fletcher:0.5.8" as fletcher: (
  diagram, 
  node, 
  edge
)

// By default, the page size is A4.
// But since I'm making a standalone diagram,
// I want it to shrink automatically shrink
// to the size of the content.
#set page(
  width: auto,
  height: auto, 
  margin: 5mm, 
  fill: rgb("#222")
)
#set text(
  fill: white
)

// This is a simple array of 
// node labels
#let nodes = ("A", "B", "C")

// This is an array of from-to edges
#let edges = (
  (0,1),
  (1,2),
  (2,0)
)

#diagram({
  for (i, n) in nodes.enumerate() {
    node(
      // Node coordinates
      (i, 0), 
      // node text
      n, 
      // node line
      stroke: 1pt+white, 
      // node id
      name: str(i)
    )
  }
  // argument unpacking
  for (from, to) in edges {
    // typed numeric variables
    let bend = 0deg
    // on the return arrow
    // arc large
    if(to < from){
      bend = 60deg
    }
    edge(
      label(str(from)), 
      label(str(to)), 
      // edge type
      "-|>",
      stroke: white,
      bend: bend
    )
  }
})

(I’ll put the rest of the typst code inside collapsed callouts).

The game board

A cool thing I hadn’t realized about the node placement in fletcher is you an either give it Cartesian coordinates like (1, 0), or you can give it polar coordinates, like (90deg, 10mm). The fletcher example diagram I was inspired by automatically chunks up the unit circle based in how many nodes there are.

#let nodes = ("A", "B", "C")

#let nodes = range(20).map(x => str(x))

#let nodes = ("A", "B", "C")

#let nodes = range(20).map(x => str(x))

So I popped the five Wh-words in English into the nodes list and got this:

Then, inspiration struck: Connecting 5 Wh-words with paths gives us the opportunity make the shape of the game board a pentagram. To make it more gamey and something the players would need to strategize, I made the paths out of each Wh-node directional, so you couldn’t get to every node from every node.

With a little more color and font flourishes, I got this final product:

#import "@preview/fletcher:0.5.8" as fletcher: (
  diagram, 
  node, 
  edge
)
#set page(
  width: auto,
  height: auto, 
  margin: 5mm, 
  fill: rgb("#393d3b")
)
#set text(
  font: "Macondo Swash Caps",
  size: 30pt
)
#set align(center)

#let nodes = ("Who", "What", "When", "Where", "Why")
#let edges = (
  (3,0),
  (0,2),
  (2,4),
  (4,1),
  (1,3),
  (0,1),
  (1,2),
  (2,3),
  (3,4),
  (4,0)
)
#box[
  #set text(size: 0.8em, fill: gradient.linear(
    color.hsv(50deg, 50%, 70%),
    //color.hsv(50deg, 50%, 70%),
    color.hsv(0deg, 100%, 100%, 100%),
    color.hsv(0deg, 100%, 100%, 100%),    
    black,
    angle: 90deg
  ),)
  The interrogation of Bartholemew Durkis
]

#diagram({
  for (i, n) in nodes.enumerate() {
    let θ = 90deg - i*360deg/nodes.len()
    node((θ, 50mm), 
      n, 
      stroke: 0.5pt, 
      name: str(i),
      shape: circle,
     fill:gradient.linear(
        luma(0%, 80%), 
        color.hsv(0deg, 100%, 100%, 70%),        
        color.hsv(0deg, 100%, 100%, 50%), 
      angle: -θ)      
    )
  }
  for (from, to) in edges {
    edge(
      label(str(from)),
      label(str(to)),
      "-|>",
      stroke: color.hsv(50deg, 50%, 70%) + 3pt,
      bend: 10deg,
    )
  }
  node(
    (0deg,0mm), 
    "Y/N", 
    name:"6", 
    shape:circle, 
    stroke:0.5pt,
    fill: color.hsv(0deg, 100%, 100%, 60%)
 ) 
  for(i,m) in nodes.enumerate(){
   edge(
     label("6"),
     label(str(i)),
     "-}>",
     stroke: color.hsv(50deg, 50%, 70%) + 1.5pt,
     bend:10deg
   )
 }
})

Combined with an animation I made separately and some mood music, I think the overall effect was good!

Reuse

CC-BY 4.0

Citation

BibTeX citation:
@online{fruehwald2025,
  author = {Fruehwald, Josef},
  title = {D\&D and {Making} {Typst} {Figures}},
  series = {Væl Space},
  date = {2025-09-13},
  url = {https://jofrhwld.github.io/blog/posts/2025/09/2025-09-13_typst-figures/},
  doi = {10.59350/ewqr3-7w333},
  langid = {en}
}
For attribution, please cite this work as:
Fruehwald, Josef. 2025. “D&D and Making Typst Figures.” Væl Space. September 13, 2025. https://doi.org/10.59350/ewqr3-7w333.