Automated Scribus Daredevils NPC 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)
-
def __init__(self, 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)
-
if scribus.objectExists(boxName):
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)
-
if layer == character.characterName:
-
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
-
if layer.startswith('Background'):
- #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)
-
if isBackground(layer):
- #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)
-
if not isBackground(layer):
-
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 thatdaredevils.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.
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.
↑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.
- Blackhawk: Blitzkrieg at North Texas RPG Con
- I’ll be running first edition DC Heroes at North Texas in 2020, on June 4.
- 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.
- Rolling random levels across a range of experience points in AD&D
- I’m going to North Texas again, and this time I’m going to run an AD&D game for levels 4-6. How can I roll a random number of experience points that will only produce characters of level 4, 5, or 6 regardless of class?
- Scribus
- Scribus is a very nice open source page layout application and includes full PDF creation. It is also scriptable using Python if you need to automate page layout tasks. Scribus is very useful for making documents that need to be shared with other editors, since anyone can get the Scribus application unrestricted.
More Daredevils RPG
- Kolchak: The Wrong Goodbye (a Daredevils adventure)
- Kolchak and crew investigates strange murders during the 1976 Christmas season. Inspired by “real” Soviet research as reported in UFO magazines of the era.
- Kolchak’s Cold January at North Texas 2024
- I’ll be running another Kolchak: The Night Stalker game at North Texas in 2024, again using the Daredevils rules from Fantasy Games Unlimited. We finally move into 1977 for the great Chicago freeze!
- Kolchak: The Big Creep (a Daredevils adventure)
- Inspired by The Powers of Dr. Remoux, The Big Creep is a Daredevils adventure for The Night Stalker set in the autumn of 1976.
- A Kolchak Christmas at North Texas 2023
- I’ll be running another Kolchak: The Night Stalker game at North Texas in 2023, again using the Daredevils rules from Fantasy Games Unlimited.
- Kolchak: The Montique Fantom (A Daredevils adventure)
- A reskin of the Daredevils adventure The Body Vanishes for The Night Stalker in 1976.
- Three more pages with the topic Daredevils RPG, and other related pages
More Programming for Gamers
- Are my dice random?
- My d20 appears to have been rolling a lot of ones, a disaster if I were playing D&D but a boon for Gods & Monsters. Is my die really random, or is it skewed towards a particular result? Use the ‘R’ open source statistics tool to find out.
- Programming for Gamers: Choosing a random item
- If you can understand a roleplaying game’s rules, you can understand programming. Programming is a lot easier.
- Easier random tables
- Rather than having to type --table and --count, why not just type the table name and an optional count number?
- Programming a Roman thumb
- Before we move on to more complex stuff with the “random” script, how about something even simpler? Choose or die, Bezonian!
- Multiple tables on the same command
- The way the “random” script currently stands, it does one table at a time. Often, however, you have more than one table you know you’re going to need. Why not use one command to rule them all?
- 12 more pages with the topic Programming for Gamers, and other related pages
More Scribus
- Save all Scribus pages as EPS
- There doesn’t appear to be a built-in way to save all pages seperately as EPS in Scribus. This script will do so.
Hey Jerry, great stuff here. I just purchased Daredevils myself. I'm hoping to run it soon. Are any of your games on YouTube? It would be great to see it in action.
Thanks, Jeff
Jeff H in Us, Michigan at 1:39 p.m. July 13th, 2022
1u/h2
Hey, Jeff. All of the Daredevils games I’ve run have been at the North Texas RPG Con. As far as I know, none of them were filmed.
Jerry Stratton in Deep in the Plexus of Texas at 2:05 p.m. July 13th, 2022
I8+2p