Idraluna Archives

Troika! Backgrounds & Monsters in Typst

I channeled my momentary Mars obsession into a Troika! hack, despite not having nearly enough experience with Troika! to justify it. Thanks to my beloved Typst, I was able to slap together a procedural layout for my half-baked ideas really quickly.

Troika!'s core building blocks are its character backgrounds and monsters, both of which follow a simple formula. Structured text that is repeated many times is a case where Typst really shines, because you can automate the layout rather than adjusting by hand for each of 36 backgrounds or whatever.

Previously, I shared simple .csv import code to populate a bestiary. For this project, I opted to store monsters and backgrounds in a .yaml file. The advantage to .yaml is that it feels much more natural to read & write, and handles line breaks and large blocks of text much better.

The bestiary yaml was structured as follows:

- Name: Banth
  Skill: 10
  Stamina: 17
  Init: 5
  Armor: 0
  Damage: as Large Beast
  Mien:
    - Playful
    - Territorial
    - Languid
    - Hunting
    - Agressive
    - Sadistic
  Desc: An extremely deadly leonine predator known to prowl the mountains and hills surrounding the dead seas of Mars.
  Quote:
    Text: >
        Its long, lithe body is supported by ten powerful legs, its enormous jaws are equipped, like those of the calot, or Martian hound, with several rows of long needle-like fangs; its mouth reaches to a point far back of its tiny ears, while its enormous, protruding eyes of green add the last touch of terror to its awful aspect. 
    Attribution: gods_of_mars
  Source: 
  Image:
    Src: /public/assets/Idraluna/Banth.svg
    Width: 80

And for backgrounds I did this:

- Name: "Terran Smuggler"
  Desc: "In addition to the soldiers & scientists, Terran colonization has introduced a menagerie of lowlifes & desperadoes to Mars, one of which is you."
  Skills: 
    - 2 Sneak
    - 1 Gambling
    - 1 Fist fighting
    - 1 Gun fighting
    - 1 Disguise
    - 1 Locks
    - 1 Awareness
  Possessions:
    - Trusty blaster pistol
    - Leather jacket
    - Dime bag of Martian cannabis
  Image: /public/assets/Idraluna/Smuggler.svg
  Imgwidth: .65

Most of the fields are pretty self-explanatory, but some notes:

So, to turn the raw monster and background data into nicely-rendered layouts, I used the following code in my main .typ file.

For monsters:

#let bestiary_yaml = yaml("Bestiary.yaml")

#for beast in bestiary_yaml {
  box(inset: (y: 1em),  grid(columns: 2, gutter: 1em,
    [=== #beast.Name
    #eval(beast.Desc, mode: "markup")

    #if "Special" in beast.keys() [==== Special 
    #beast.Special]

    #if beast.Image.Src != none {figure(placement: auto, caption: none, image(beast.Image.Src, width: beast.Image.Width * 1%))} else if beast.Quote.Text != none {quote(attribution: cite(label(beast.Quote.Attribution)), block: true, eval(beast.Quote.Text, mode: "markup"))}
  ], [
    #smallcaps([Skill: #beast.Skill]) \
    #smallcaps([Stamina: #beast.Stamina]) \
    #smallcaps([Initiative: #beast.Init]) \
    #smallcaps([Armor: #beast.Armor]) \
    #smallcaps([Damage #beast.Damage]) \

    ==== Mien
    
    #enum(..beast.Mien)
  ]
  ) +
  if beast.Quote.Text != none and beast.Image.Src != none {quote(attribution: cite(label(beast.Quote.Attribution)), block: true, eval(beast.Quote.Text, mode: "markup"))} +
  if beast.Source != none {cite(label(beast.Source), form: none)}
  )
  
  v(1fr)
}

And for backgrounds:

#let background_yaml = yaml("Backgrounds.yaml")
#let background_count = counter("bg")

#for background in background_yaml {
  background_count.step()
  let info = grid(
    columns: 2, gutter: 1em,
    [
      === #context dxy_map(background_count.get().at(0)) #background.Name

      ==== Advanced Skills
      #for skill in background.Skills [#skill \ ]

      #if "Special" in background.keys() [
        ===== Special
        #background.Special
      ]
    ],
    [
      #eval(background.Desc, mode: "markup")

      ==== Possessions
      #list(..background.Possessions)
    ]
  )
  box(inset: (y: 1em),
  if background.Image != none {
    grid(columns: (3fr, if "Imgwidth" in background.keys() {background.Imgwidth * 1fr} else {1fr}), gutter: 1em,
      info,
      figure(image(background.Image))
    )
  } else {info})
  v(1fr)
}

The results are as follows:

Screenshot from 2026-06-08 21-16-02

Screenshot from 2026-06-08 21-16-29

To break it down:

Additional notes:

Anyways, it might seem a bit convoluted, but the beauty is that writing up new entries into the .yaml file is a lot easier than manually typing the text into a layout template. And, if I decide that WARHOON! would be better as an A5 zine I can always reconfigure the display functions to something that works best for the new format.

#DIY #Typst #troika