Biblyon the Great

This zine is dedicated to articles about the fantasy role-playing game Gods & Monsters, and other random musings.

Gods & Monsters Fantasy Role-Playing

Beyond here lie dragons

Automated Scribus Daredevils NPC character sheets

Jerry Stratton, July 7, 2021

Scribus Daredevils character sheet

Scribus is great for creating RPG character sheets.

In part 1, the Daredevils NPC generator, I showed how to create simple character data in a form that makes it useful for a simple character sheet. It’s nice, however, to provide players a nice cardstock pregen with a familiar layout. I deliberately made that simple character sheet output from the daredevils script provide the data in a form that makes it easy to import into other software.

I chose to import it into Scribus, an open-source desktop publishing application that creates great PDF files and can be automated using the Python programming language. Scribus runs on macOS, Linux, and Windows.

Scribus has a Script menu; you can choose to “Execute” any Python script on your computer. I keep mine in ~/bin/Scribus, which is to say, in a folder called Scribus in a folder called bin in my macOS user account. You can put them anywhere. An obvious location would a Scribus folder in your Documents folder.

For the Kolchak game, I used a script I called daredevils.py to import the character sheets into Scribus, creating a new layer for each character. The bulk of the work is done in a class called Sheet. Here’s the start of that class:

[toggle code]

  • class Sheet:
    • def __init__(self, characterName):
      • self.skillsRect = self.getRect('skills')
      • self.backgroundItems = []
      • self.quotes = []
      • self.characterName = characterName
      • self.openSection('aspects')
      • # create the layer for this character sheet
      • scribus.gotoPage(1)
      • if characterName in scribus.getLayers():
        • scribus.setActiveLayer(characterName)
      • else:
        • scribus.createLayer(characterName)
      • self.createBox('Character', characterName)

The line def __init__ marks initialization code for whatever the class represents. In this case, the class is a character sheet. As I noted in the previous article, “class” in programming has nothing to do with character classes in role-playing games. A programming class is basically collection of variables and functions—called properties and methods when they’re part of a class—meant to provide functionality for one thing. In this case, that one thing is a Daredevils character sheet. So the initialization code runs whenever I create a new character sheet.

Everything that begins with self is a method or a property on the class. For example, self.skillsRect is a property. It stores the location on the character of the next skill. The abbreviation rect is often used for a rectangular location in programming. In this case, it consists of an x and y location, and a width and height, all in inches. The “x” is measured from the left of the paper and the “y” is measured from the top.

And self.getRect() is a method. It takes the parameter given it (in this case, the string “skills”) and returns the rectangle for that item. The rectangles are all stored in an array that looks like this:

[toggle code]

  • boxes = {
    • 'character':    (4.9,   .625, 3.5, .27),
    • 'player':       (4.9,   .875, 3.5, .27),
    • 'nationality':  (4.9,  1.125, 3.5, .27),
    • 'career':       (4.9,  1.375, 3.5, .27),
    • 'episode':      (4.9,  1.625, 3.5, .27),
    • 'skills': (4.5, 2.25, 2.8, .24),
  • }

The character’s name starts at 4.9 inches across and .625 inches down; it is 3.5 inches wide and .27 inches tall. The “skills” section starts at 4.5 inches across, and 2.25 inches down. I store this in a property on creating the character sheet so that it can be updated every time a new skill is added to the character. Every time a new skill is added, the second number—that is, the distance from the top of the page where the next skill goes—is increased by .24 inches.

Here’s how text boxes get created:

[toggle code]

  • def createOrUseBox(self, boxName, location, value, alignment=None):
    • if scribus.objectExists(boxName):
      • scribus.moveObjectAbs(location[0], location[1], boxName)
      • scribus.sizeObject(location[2], location[3], boxName)
    • else:
      • scribus.createText(location[0], location[1], location[2], location[3], boxName)
    • scribus.setText(value, boxName)
    • if alignment:
      • scribus.setTextAlignment(alignment, boxName)
Scribus Script menu

You can run Python scripts in Scribus that will act on the current document.

If the box already exists, the script makes sure it’s in the correct location with the correct size. Otherwise, it creates a new text box with that name at the desired location. It then sets the contents of that box and, if desired, the text alignment of the box.

Scribus can take more complex text boxes than just single-line ones created by the createOrUseBox method. Here’s how the boxes on the back of the sheet, the backgrounds and quotes, get created:

[toggle code]

  • def createLongTextBox(self, boxName, boxRect, textItems):
    • scribus.gotoPage(2)
    • text = "\n".join(textItems)
    • self.createOrUseBox(boxName, boxRect, text)
    • scribus.setColumns(2, boxName)
    • scribus.setColumnGap(.25, boxName)
    • scribus.setParagraphStyle('Long Text', boxName)
    • scribus.hyphenateText(boxName)
    • #resize as needed
    • while scribus.textOverflows(boxName):
      • boxRect[3] += .2
      • if boxRect[1] + boxRect[3] > pageBottom:
        • die('Warning', 'There is too much long text for page two')
      • scribus.sizeObject(boxRect[2], boxRect[3], boxName)

You can see it uses the createOrUseBox method to create the initial box. But then it sets the number of columns in the box to two, sets the gap between the columns, sets the paragraph style for text in the box, and hyphenates the text in the box.

It then resizes the box so that it fits the text that’s been put into it. The method scribus.textOverflows() checks to see if that box has too much text. If it does, the script goes ahead and increases the height slightly, and it keeps doing this until there is no more overflow, or the box has become too big for the page.

This is a big script, and you don’t need to know everything about it to use it or even to modify it. But I will go over the script’s basic behavior. Here’s the top of the script:

[toggle code]

  • dataDir = os.path.expanduser('~/Desktop/Daredevils/data')
  • importScope = scribus.messageBox(u'Import all?', u'Import all files from ' + dataDir + u'?', button1=scribus.BUTTON_YES, button2=scribus.BUTTON_NO, button3=scribus.BUTTON_CANCEL|scribus.BUTTON_DEFAULT|scribus.BUTTON_ESCAPE)
  • if importScope == scribus.BUTTON_YES:
    • allCharacters = True
  • elif importScope == scribus.BUTTON_CANCEL:
    • sys.exit()
  • else:
    • allCharacters = False

The first line sets the import directory—the “dataDir”—to a specific location. When I’m creating a bunch of new sheets, I am doing it so often I don’t want to be asked where the sheets are. So, instead, I just hardcode the directory into the script. In this case, the location was a “data” folder in the “Daredevils” folder on my iMac’s Desktop. You can change it to wherever you store your own Daredevils character data, or change it to ask for a location using scribus.fileDialog (see below).

I did eventually have the script ask me if I want to import all of the characters or just an individual character. That question has a YES, a NO, and a CANCEL option. YES sets the variable allCharacters to True, CANCEL just exits the script, and NO sets the variable allCharacters to False.

Here’s the main loop:

[toggle code]

  • #don't do anything unless there's a document open
  • if scribus.haveDoc():
    • os.chdir(dataDir)
    • if allCharacters:
      • characterFiles = sorted(glob.glob('*.txt'))
      • scribus.progressReset()
      • scribus.progressTotal(len(characterFiles))
      • for characterFile in characterFiles:
        • scribus.progressSet(characterFiles.index(characterFile)+1)
        • #allow for progress to update
        • time.sleep(.1)
        • character = Character(characterFile)
        • character.populate()
      • scribus.progressReset()
    • else:
      • characterFile = scribus.fileDialog('Character to import', filter="*.txt")
      • if not characterFile:
        • sys.exit()
      • character = Character(characterFile)
      • character.populate()
      • #hide every character except this one or it gets really confusing
      • for layer in scribus.getLayers():
        • if layer == character.characterName:
          • scribus.setLayerVisible(layer, True)
          • scribus.setLayerPrintable(layer, True)
        • elif not layer.startswith('Background '):
          • scribus.setLayerVisible(layer, False)
          • scribus.setLayerPrintable(layer, False)
  • else:
    • scribus.messageBox("No Open Document", "You need to have a character sheet document open to create character sheets.")

If the allCharacters variable is True, the script preps Scribus’s progress bar and loops through each file in the data folder.1 It first sorts the files, so that they’re handled in alphabetical order. That’s a little thing, but it really helps when watching the script work to know that there’s an understandable order to the imports.

If, on the other hand, the allCharacters variable is False, the script pops up a dialog to ask which character I want to import. It then imports that one file into a character sheet and, in addition, displays that character sheet when it’s done. It loops through every layer and hides any layer that is not either the currently-imported character, or that is not background.

I name the background layers so that they start with the word “Background”. This makes it easy to do stuff like that. It also makes it easy to create PDF files from every character sheet in a few seconds. I have another Scribus script that I call pregens2pdf.py for that. I’ve used this script for several years now, including last year’s Blackhawk game and the Fell Pass AD&D game I ran three years ago.

The PDF generator literally just loops through every layer. If the layer is not named as a Background layer, the script prints it, along with the backgrounds.

[toggle code]

  • #!/usr/bin/python
  • # -*- coding: utf-8 -*-
  • import scribus
  • import os
  • def isBackground(layer):
    • if layer.startswith('Background'):
      • return True
    • return False
  • #don't do anything unless there's a document open
  • if scribus.haveDoc():
    • #get the folder to save to
    • saveTo = scribus.fileDialog('Folder to save characters to:', isdir=True, issave=True)
    • if saveTo:
      • #deselect, or it will only print the current selection
      • scribus.deselectAll()
      • layers = scribus.getLayers()
      • #first, make sure only the background is set to print
      • for layer in layers:
        • if isBackground(layer):
          • scribus.setLayerPrintable(layer, True)
        • else:
          • scribus.setLayerPrintable(layer, False)
      • #now, go through each non-background layer and export as PDF
      • exporter = scribus.PDFfile()
      • for layer in layers:
        • if not isBackground(layer):
          • scribus.setLayerPrintable(layer, True)
          • filePath = os.path.join(saveTo, layer + u'.pdf')
          • exporter.file = str(filePath)
          • exporter.save()
          • scribus.setLayerPrintable(layer, False)
  • else:
    • scribus.messageBox("No Open Document", "You need to have a document open to save it as PDF.")

This script uses scribus.fileDialog to ask for the folder to save the PDFs in. It then loops through all of the layers to save them as PDFs in that folder. It hides all of them except the background layers. Then, it loops again through each layer, setting each in turn to printable and exporting it to PDF.2

The Daredevils NPC character generator archive (Zip file, 16.8 KB) now includes:

  • An empty Daredevils character sheet for Scribus, Daredevils.sla;
  • the daredevils.py importer for importing data into Scribus;
  • the daredevils script from the previous article for creating the data that daredevils.py needs;
  • and the pregens2pdf.py script for creating PDFs from Scribus layers.

If you’re looking to create a bunch of pregens for a convention game, Scribus and Python are a great way to ensure well-designed, error-free character sheets.

In response to Daredevils NPC generator: Part 1 of 2: a script to calculate Daredevils attributes, talents, skills, and stats from a text file of initial values and character development.

  1. On the command line, or in programming, folders are usually referred to as directories. Or, depending on your perspective, in the GUI, directories are usually referred to as folders.

  2. There’s an odd bit in the export process. Scribus’s PDFFile method requires a string. In the past, this meant it would screw up if handed unicode text. This in turn meant going through a more complicated process of saving to a simple filename and then renaming to the correct filename, so that quotes and accented characters didn’t end up as garbage in the filename. In Scribus as of at least 1.5.7, this is no longer necessary. But I’m not sure why, as it really shouldn’t be possible to convert a unicode to a string while maintaining the unicode characters.