Strange Eons

Creating a DIY Component

Send Feedback      Home Page > Strange Eons >Miriam's Basement > Creating a DIY Component

DIY Components
Let's Get Started
OK, Let's Make Something!
Your Turn

DIY Components

A DIY component is a special type of component that you design yourself using script code. While other kinds of customization allow you to modify an existing kind of component, DIYs are not based on something else. You decide what information the user provides, what the editor controls for that information are like, and how the card is drawn.

Preparation

You'll want to review the basics of creating a project and writing an extension plug-in: what an extension is, what files are required, how to pack them up. You should also be familiar with concept of settings keys and private settings on a component. And you will also want to download and unpack/install the following:

  1. The latest version of Strange Eons.
  2. A copy of the latest Plug-in Authoring Kit. It has lots of useful example code and documentation you can refer to for help.
  3. Unpack a copy of the Strange Eons resources for reference.
  4. Install the latest versions of the following plug-ins: Image Resource Browser, Region Editor, Script Library Documentation Browser, Show Regions.

A Refresher

Before we get going, let's review a few concepts, starting with scripting libraries. Strange Eons includes a number of these libraries: prewritten script code that you can use in your own scripts. These libraries are stored in the resources/libraries folder (in the Strange Eons resources). To use a library, you just add a uselibrary( "libraryname" ); line to the top of the file, and all of the library code will be available to you in the rest of the script.

All of the script libraries have matching documentation. You can find a copy of this documentation in the Plug-in Authoring Kit or you can read it directly from within Strange Eons using the Script Library Documentation plug-in. Using the plug-in has the advantage that it always shows you the documentation for the version of the libraries actually available from your copy of Strange Eons.

For more details on how settings work, see the Settings entry on the Glossary page of the Plug-in Kit documentation.

The second concept that we'll review is the idea of settings. Settings in Strange Eons consist of pairs of keys (the names of the settings), and values (what the settings are actually set to). There is a single shared group of settings that define all of the default values for the different settings keys, and in addition each component can have private settings that override the shared setting value just for that component. In the Enemy Card Customization Example, you can see how to modify a component's private settings to customize the text and images it uses. We'll use private settings to store the user's choices for the various features of our card. For example, if our card requires a value for how much gold the card is worth in-game, then we might store that as a setting with the key Gold.

The final concept we need to go over is that of a region. A region is a particular kind of setting, one that describes a rectangular area by giving the location of its upper-left corner (x-offset and y-offset) along with its width and height. The value of a region consists of these four numbers separated by commas. For example, the sanity-text-region describes the rectangle within which the "Sanity" label of investigator cards is written. If the value for this key was "525,255,119,39", then it would have this interpretation:

The Region Editor plug-in makes it easier to fine tune the placement of regions in place on a custom component. There are also project actions and file types to edit regions graphically.

Here the region is shown as a purple box. Remember, the first number is how far to the right of the upper-left corner the rectangle begins; the second number is how far down from the upper-left corner the rectangle begins; the third number is how wide the rectangle is, and the fourth is how tall. Using the Show Regions plug-in you can selectively highlight regions on a component with purple boxes during development.

Let's Get Started

First we'll get familiar with DIY components, then we'll look at packaging the whole thing up in an extension. To start with, find the diy folder of the Plug-in Kit. Among other things, you'll find a file called minimal-diy.js. Open the Quickscript window and drag minimal-diy.js into it to open it. Run the script and observe what happens. You should see a new editor, like this:

In that single script file is all of the magic needed to create a whole new kind of card. Granted, it isn't a very exciting card. But it's a start. The script itself works something like a plug-in script: we define some functions with particular names, and Strange Eons will call those functions when it needs to. Let's start by understanding what the functions are for and when SE calls them.

The Anatomy and Lifecycle of a DIY Component

Below is a diagram that shows the basic lifecycle of a DIY component. It starts when the component is first added to SE, either by creating a new one or by opening an existing one that is stored in a file. Let's go through the diagram, starting from a new file, and discuss each function in turn.

The Basic DIY Lifecycle, and the Functions Called by Strange Eons
Click the diagram for a more detailed, annotated version.

create( diy )

This function is called exactly once, the first time that the component is ever created. You have two jobs to do before it returns. First, the new DIY object that you are passed has a number of basic attributes that will determine the component's essential character. You can only change those attributes in this function or in onRead. Second, you must set initial values for all of the properties that the user will be able to edit later (like Name, Gender, Speed—whatever is appropriate for the card).

For a complete description of the available attributes, see the documentation for the diy scripting library.

Some of the key attributes that you will need to set are the face style, the front and back templates, and the portrait key. The face style controls how many and what kind of faces the card will have.  To change it you must set diy.faceStyle to one of these possible values:

FaceStyle.ONE_FACE the component has only one face (a front but no back)
FaceStyle.PLAIN_BACK the component has a plain back (a fixed image that SE draws for you)
FaceStyle.SHARED_FACE the component has two faces, but they are identical
FaceStyle.TWO_FACES the component has two different faces (you get to draw both sides)

The front and back templates are images that define the basic look, shape, and size of the component. Back templates work the same as front ones, but they are for the back face of the card instead of the front: I'll just talk about front templates here. To set the front template for a card you set diy.frontTemplateKey to a base name for a family of settings keys. By combining the base name with different suffixes, the DIY card will compose a number of settings keys and use their values to determine what it needs to know about the front face of the card. The most important of these are as follows (where xxx is the value assigned to diy.frontTemplateKey ):

xxx-template the resource file that contains the image
xxx-dpi if present, the resolution of the template image in pixels per inch (2.54cm) (default is 150): the physical size of the card is determined by the size of the template image (in pixels) divided by this value
xxx-expsym-region if present, the region where the expansion symbol is drawn (default is no expansion symbol)

If we want our component to include a portrait image, then we need to set diy.portraitKey as well. Like the front and back template key attributes, this is also a base key to which various suffixes are appended. See the diy library documentation for details.

A DIY component typically has some additional properties chosen by the plug-in designer: values that the user can change that will affect how the card is drawn. For example, Investigator cards have a Stamina value which is a number that indicates the character's physical toughness. On a DIY, the Stamina value would be an attribute property. Every DIY component includes a name and comment property. Other properties need to be added, and the way that they are usually added is by storing them as private settings on the card. In the create function, we give these properties the default values that they should have when the user first starts editing the card. For example, we could set an initial stamina value of 5 with a line like the following:

// new investigators will start with a Stamina of 5
diy.settings.set( "Stamina", "5" );

Be careful not to pick a name that is the same as a standard Strange Eons setting that might be needed by your card. Anything that has at least one capital letter in it will be safe. Starting with SE 2.00.5, the scripting system lets you access settings keys as if they were regular variables by writing a $ followed by the name of the key. (You also have to use _ instead of - because hyphens aren't allowed in variable names.) So the above could be written as the easier to follow:

    $Stamina = "5";
and have the same effect. DIY cards will access the DIY's private settings with this syntax, while other scripts will normally access shared settings.

Note that a DIY's standard name and comment properties are not set this way because they are part of the DIY object proper and not a private setting: they are accessed directly with diy.name and diy.comments.

createInterface( diy, editor )

This function is called to construct the user interface for the component, and to bind user interface controls to their matching settings. This is done using a Bindings object, from the uibindings standard library. A Bindings object converts between the edits the user makes in the edit controls and the private settings that store the properties being edited. (It will automatically create appropriate event listeners and attach them to the edit controls.)

The basic sequence of events for creating a user interface is as follows:

  1. Create a container that will hold and lay out the user interface controls (see the uilayout library docs).
  2. Create a new Bindings object that will translate between controls and properties in the private settings (see the uibindings library docs).
  3. Create one or more user interface controls (see the uicontrols library docs).
  4. Add the controls to the container.
  5. Add the controls to the Bindings object, and for each one tell the Bindings object: (1) what the settings key is for the property edited by the control; and (2) which card faces need to be redrawn when the property changes.
  6. Tell the diy object which text field control, if any, will be used to edit the component's name. This is done by calling diy.setNameField( nameField );. (Recall that the component's name is a special built-in property, so it isn't managed through Bindings.)
  7. Call the container's addToEditor function to add the container to the editor window as a new tab.
  8. Call the bind function of the Bindings object to create and install the "glue code" that links the controls and the private settings.

When the editor window is finally opened, the bindings will be used to copy the initial property values from the component's private settings into the controls.

createFrontPainter( diy, sheet ), createBackPainter( diy, sheet )

These functions are called to give you a chance to do any setup that is needed before a card face can be painted. Note that createBackPainter is only ever called if you use the FaceStyle.TWO_FACES face style. The most common use of this function is to create and set up markup boxes. A mark-up box is used to draw Strange Eons mark-up (text with tags).  See the markup library docs for details on MarkupBox objects.

paintFront( g, diy, sheet ), paintBack( g, diy, sheet )

These functions are called when their respective card face needs to be drawn. Note that paintBack is only ever called if you use the FaceStyle.TWO_FACES face style. The g parameter is a Graphics2D graphics context object that can be used to paint the card face. The graphics context will be scaled so that 1 unit is equal to 1 pixel on the template image, regardless of the resolution that the card is being drawn at. The sheet parameter is the DIYSheet instance that is responsible for managing the card face you are about to draw. It provides some helper functions that simplify some of the common tasks associated with drawing a card, such as:

// clip, scale, translate, and paint the current portrait image
sheet.paintPortrait( g );

// paint the template image defined by this faces's template key
sheet.paintTemplateImage( g );

Once you have drawn the portrait (if any) and the card's template image, the basic procedure is to "do something" which each of the component's properties. The "something" will depend on what the card face is supposed to look like. A typical action is to use a previously set up mark-up box to lay out and draw text taken from one of the properties, which might look something like this:

// set text mark-up text to draw from the special-ability private setting (property)
abilityBox.markupText = $SpecialAbility;
// draw in the region defined by the setting blah-card-ability-text-region
abilityBox.draw( g, diy.settings.getRegion( "blah-card-ability-text" ) );

After this function returns, Strange Eons will automatically execute the on-paint event handler for the card (if one is set) and paint the expansion symbol (if an expansion is selected and the template's expansion symbol region key is defined).

onClear( diy )

This function is called when the user issues a Clear command. The component's name and comments will be cleared automatically. You'll need to add the code to clear the rest of the component's properties yourself. For example:

function onClear( diy ) {
    // reset card to a "clear" state; what
    // that means is up to you
    $Strength = "1";
    $Wits = "1"; 
    $Gold = "0";
    $SpecialAbility = "";
}

Regarding onRead and onWrite

These functions let you take special actions when the card is read from or written to a file. We don't need them for our example, but I'll describe them so you will know when to use them. There are two main uses for these functions. First, you can use them to update old save files when you release a new version of your plug-in. For example, if you add a new property in the updated version, old save files won't have that property. The new plug-in's onRead should detect this and set the missing property to a suitable default value. The DIY.cardVersion property can be used to help you determine which features need to be updated. The diy library documentation outlines how to do these kinds of updates.

Serialization and Script Objects
The Java classes that are used to represent JavaScript objects can't be read and written directly through an object stream. However, you could write functions to read and write the object's properties as a sequence of primitive values and then call those functions from onRead/onWrite.

The second reason to use these functions is to store data in the save file that can't be easily represented as a text string. This is an advanced feature, so don't worry if you don't understand the rest of this paragraph. Settings values are always stored as text strings. Although many kinds of data are easily converted to and from text, some are not. For example, the DIY system provides standard handling for a portrait image, but what if you want to include some other user-provided image? The object input stream (passed to onRead) and object output stream (passed to onWrite) allow you to read and write raw data as part of the save file. They can read and write Java primitive types (readInt, writeInt, etc.) and Java objects that implements the Serializable interface. As of 2.1 alpha 11, you can read and write images using readImage and writeImage, respectively.

OK, Let's Make Something!

Now that we know what the basic parts of a DIY component are, and how they work together, let's try our hand at making one. To keep things simple, we'll stick with graphics and fonts that are already in Strange Eons. That way we can focus on getting the hang of DIYs. For our project, let's suppose that we want to take the "Plot" example provided with the built-in "Miscellaneous Large" card type, and create a dedicated card type for it:

The Miscellaneous Large example creates this card with a single large block of mark-up, but we'll create a card with specific fields for the background, effects, and foil, and we'll build in the title and divider decoration.

To start with, create a new project and add an empty plug-in task. By starting from an empty plug-in, you'll get a good understanding of how everything fits together. Then when you use some of the more automated task types later, there won't be any mysterious files that you are scared to change.

Inside the task folder, make a new folder called resources and then make a new folder inside of that called plotexample. This is where we will put our scripts. If we had our own images or other resources, we'd put those here, too.

On the plotexample folder, right click and choose New | Plug-in Script. Call it extension.js (the .js is already set, so don't actually type that). This will create a skeleton script file that includes dummy definitions for the functions used by a plug-in. We'll get back to this file in a moment.

Since we started from an empty plug-in, we'll need to create a root file for our plug-in. A root file is looked at by Strange Eons when it loads the plug-in. It says which class or script to use to start the plug-in. Right click on the task folder and choose New | Plug-in Root. This will create the root file and then immediately open the root editor. In the root editor, click on the extension.js script file (the only choice, since it is the only script), then click Update. Be sure you don't rename or move the root file, or the plug-in won't work.

Now we're ready to edit the plug-in script. Double-click on extension.js to open it. This is a general purpose plug-in script. We'll change it quite a bit to suit our purposes. First, because we are creating an extension, we need to include the extension library by adding the line uselibrary( "extension" ) to the top of the script. Second, we won't need the run, hide, isShowing, or unload functions, so we can delete those. Next, we fill in appropriate return values for getName and getDescription to describe our plug-in. Finally, the main thing that our plug-in script needs to do is tell Strange Eons that we want to add a new card type. To do that we'll add the line GameData.parseEditors( "plotexample/plot.classmap" ); to the bottom of the file. This tells SE to make new components available from descriptions in the plot.classmap file (we'll make that momentarily). The result should look like this (I've deleted the comments for brevity):

uselibrary( "extension" );

function getName() {
    return "Plot Cards";
}

function getDescription() {
    return "A DIY Example that adds plot cards.";
}

function getVersion() {
    return 1;
}

function run() {
    println( "Plug-in activated" );
}

GameData.parseEditors( "plotexample/plot.classmap" );

Save the script file, then right click on the plotexample folder and choose New | Class Map. Name this plot.classmap (again, don't fill in an extension since it is provided). Read through the comments at the top of the file to get a sense of how class maps work, then change the last line of the file to read:

DIY Plot Card Example = diy:plotexample/plot-card.js

Save the file. We've now created the basic infrastructure that the plug-in needs to add our new component type. All we need is the script that actually makes and controls the card. If you read the comments in the class map file, you can guess that we are going to create another script in the plotexample folder, this time called plot-card.js. To create it, choose New | DIY Component Script and enter the desired name. Double-click on the script to open it.

Writing the DIY Script

The skeleton script created by Strange Eons is already a functioning DIY component. It is very similar to the minimal-diy.js example we looked at earlier. Try running it. There are two ways to do this: either right click on the script file in the project view, or right click on the open script editor. Choose Run. If you right click on the open script editor, it will save the file automatically prior to running it. Close the card that we created by running the script.

Let's really strip things to the bare minimum. In the script editor, select all text (Ctrl+A), then delete it. Copy and paste in the following, which includes just the diy library and all of the functions we'll need to write:

uselibrary( "diy" );

function create( diy ) {
}

function createInterface( diy, editor ) {
}

function createFrontPainter( diy, sheet ) {
}

function paintFront( g, diy, sheet ) {
}

function createBackPainter( diy, sheet ) {
}

function paintBack( g, diy, sheet ) {
}

function onClear( diy ) {
}

function onRead( diy, ois ) {
}

function onWrite( diy, oos ) {
}

testDIYScript();

The final line, testDIYScript(); calls a special function that lets us test the DIY component while we are writing it without having to build a plug-in bundle and start a copy of Strange Eons each time we want to test. It is important that we remember to comment it out or delete it when we are done testing. We don't want it to appear in the finished plug-in.

As with the skeleton script, we can run this script right now and get an editor. Try it! Not surprisingly, our card is nothing but a black box, and the only control we have lets us edit the comments (one of the two built-in "properties").

Let's see if we can write enough code to get the background from the Miscellaneous Large card. First, we'll need to set up the DIY object in the create function to set the card's basic attributes. Choose Toolbox | Script Library Documentation and click on the diy entry. Hunt around until you find the description of the create() function. Read it over once (it's fairly long because it describes a lot of different options for setting up the card.

Remember that each time you run the test script it will create a new editor. Every once in a while as you work, take a moment to close the extra editors from previous test runs!

We can pick off a couple of the DIY properties right away. We'll set the version to 1, and we'll set the extension name to PlotExample.seext, because that is what we'll call the finished extension. To start with, let's set the face style to just one face so that we can work on one thing at a time. Now we just need a key to use for the front template, and we can get started. Open the folder where you keep your unpacked copy of the Strange Eons resources. In the main resources folder, search through the card-layout.txt and find the keys for the Miscellaneous Large card. There are two different sets of keys; one for the portrait orientation version and one for the landscape orientation version. We'll use the portrait version. Reading the DIY library documentation, we see that we want to use the name of the key that names the image file to use, less the -template suffix. We should end up with the following for our create function:

function create( diy ) {
    diy.cardVersion = 1;
    diy.extensionName = "PlotExample.seext";
    diy.faceStyle = FaceStyle.ONE_FACE;
    diy.frontTemplateKey = "lg-misc-front-sheet";
}

If we wanted to use our own custom image for the card, we'd have to add appropriate keys to the program settings and put the images we want to use in with the rest of the files for our extension. For testing, we would pack up a dummy version of the extension that includes the images we want to use and run Strange Eons with it. Then the images would be visible to our script as we worked on it.  (Alternatively, we could build the plug-in bundle and then use the test command in our project.) To define the keys we would need, we could add code like this to the start of our test script:

Patch.temporary(
    "our-card-type-front-template", "ourextension/front.png",
    "our-card-type-front-expsym-region", "32,32,15,15"
    // etc.
);

// Note that, alternatively, we could use $ notation instead:

$our_card_type_front_template = "ourextension/front.png";
$our_card_type_front_expsym_region = "32,32,15,15";

Once happy with the results, we'd either move this code to the main extension file or create a separate settings file and load that file from the extension script using a line like GameData.parseSettings( "plotexample/card-layout.txt" ). Check out the other DIY examples in the Plug-in Kit to see how this can be done. For now, back to the problem at hand. The code for our create function sets the right key to get the image we want, but if we run it we still won't see anything. Strange Eons won't actually paint the image for us. This is done on purpose, because often we want to paint some things underneath the template image. For example, if we have a card type that will have a portrait, we would normally paint the portrait first, and then paint the template image (which has a suitable transparent area cut out of it) overtop. As we learned earlier, the function paintFront is called when it is time to paint the card. Edit the function as follows to paint the template, then try running the script:

function paintFront( g, diy, sheet ) {
    sheet.paintTemplateImage( g );
}

The other task that we need to take care of during create is to set the initial values for the various properties that the user will be able to edit. This sets up the example card that will greet the user when they create a new card. To do that, we first need to decide what those properties will be and what kind of control the user will have over them. For example, can the user put in any value they want, or do their choices need to be restricted to a fixed list? For this card type, the answer is fairly simple. We'll add let the user enter a name, summary, effects, and a foil. (The name will replace the title "Plot".) For all of these, the user will be allowed to enter any mark-up text that they want, but the name should probably just be one line.

All DIY cards include a name and comments, so we don't need to do anything special to add those. We can access them using diy.name and diy.comment, respectively. Any additional properties that we define will require just a little more work. We could make script variables for our properties, but a problem arises when we want to save the component and open it again later. When we open the file, the script won't pick up where it left off, but will instead be run from scratch. So any properties that we store in script variables would be lost. We could write onWrite and onRead functions that write out the values of the variables, but this can be tricky and hard to maintain. Instead, we will store our properties in the component's private settings, which are already saved and restored automatically for us. To make this easy, Strange Eons provides a special $ notation for script files. When a variable name begins with a $, its value is mapped to the game setting with the same name (less the $). The only trick is that JavaScript variables can't have a hyphen (-) in them, so you have to use an underscore (_) instead. For example, $sanity_text = "Brains!"; sets the value of the setting key sanity-text to Brains!. To make things even easier, when we run a DIY script Strange Eons automatically switches to the component's private settings for this notation.

So much for storing the card properties. All we have to do now is decide what to call them. To avoid conflicting with the names of existing setting keys, I suggest that you start the name of each attribute with a capital letter and that you use "camel case" for multiword attribute names ($JustLikeThis). Here is the code to initialize the attributes with the same example values that the Miscellaneous Large card uses, except that we'll give the card a title other than "Plot" and leave the comments blank for brevity:

function create( diy ) {
    diy.cardVersion = 1;
    diy.extensionName = "PlotExample.seext";
    diy.faceStyle = FaceStyle.ONE_FACE;
    diy.frontTemplateKey = "lg-misc-front-sheet";
    
    diy.name = "Shaping Young Minds";
    diy.comment = "";
    $Summary = "Cultists have converted a popular and influential "
        + "professor to their cause.";
    $Effects = "Clue tokens may not be gained by any means at any Miskatonic "
        + "U. location. If the terror level reaches 4, immediately open a "
        + "gate at the Science Building as if a gate burst had occurred there.";
    $Foil = "Instead of having an encounter at the Administration Building, "
        + "you may make a <b>Will< >(-2)</b> check to see the Dean. If you "
        + "pass, discard 5 Clue tokens to convince him that the professor is "
        + "dangerously insane and should be hospitalized. If no gate is open "
        + "on the Science Building, seal it using a token from the doom track.";
}

While we are at it, we may as well write our onClear function, too. This gets called when the user chooses Edit | Clear, and it should set all of the component's attributes to a reasonable neutral state. Attributes that allow any text should usually be cleared to empty strings, but if you have an attribute that must be one of a fixed set of options, then it should be set to one of those. The component's name and comments will be blanked out for us, so here is our implementation of onClear:

function onClear( diy ) {
    $Summary = "";
    $Effects = "";
    $Foil = "";
}

Now that we have the basics in place, let's build the user interface. Advanced developers can construct an interface "by hand" using Swing objects, but Strange Eons provides a number of scripting libraries to make things easier. Because they are often used together, there is a convenience library that will include them all at once for us. Let's include it at the top of the file:

uselibrary( "diy" );
uselibrary( "ui" );

Remember the eight steps that we listed above for setting up the interface? We'll go through them now one at a time.

1. Create a container to lay out the controls.

The uilayout library provides several kinds of containers. A container is used to group controls together and it controls how they will appear to the user. We'll use the most flexible kind of container, a Grid. (To use the Grid container in versions of SE prior to 2.1, the user must have installed the ui-grid.selibrary. Starting with 2.1, this library is built in.) We'll start by creating one and assigning it to a variable:

var container = new Grid( "fillx" );

The "fillx" string will cause the container to grow to fill entire width of the tab.

2. Create a new Bindings object.

A Bindings object is used to build and manage the relationship between the user interface controls and the  state of the component. It will do the work of listening for the user to modify the interface controls and updating our attributes (private settings) to match. We'll add this line to create our Bindings object:

var bindings = new Bindings( editor, diy );

3. Create the user interface controls.

The uicontrols library defines a number of helper functions to create common kinds of interface controls. We can use it to create drop-down lists, text fields, check boxes, and more. We'll use a text field to store the name, and text areas (text fields with more than one line) for the others:

var nameField = textField( "", 30 );
var summaryField = textArea( "", 4, 30, true );
var effectField = textArea( "", 7, 30, true );
var foilField = textArea( "", 7, 30, true );

4. Add the controls to the container.

You can do more with a container than use it to make DIY tabs. For example, it is an easy way to create a dialog box in a plug-in. Have a look at the uilayout library documentation for more details.

The container object is used to arrange the controls into a group. It determines where the controls will appear within the editor tab, what order they will appear in, their size, and so on. You add controls to a layout using either the add or place methods. The difference is that when you use place, you also supply a string that gives "hints" about how to lay out the control. The available hints depend on the kind of container. Here is the code we'll use:

container.place(
    "Title", "split", nameField, "growx, wrap",
    "Summary:", "wrap",
    summaryField, "gap i, growx, wrap",
    "Effects:", "wrap",
    effectField, "gap i, growx, wrap",
    "Foil:", "wrap",
    foilField, "gap i, growx, wrap"
);
container.setTitle( "Plot" );

You'll notice that the first "control" that we add is the string "Title". Wherever you can add a control to a container you can add a string instead, and the string will be converted into a label control (that is, a swing.JLabel) for you. For Grid layouts, the wrap hint ends a row of controls and puts the next control at the start of the following row. The growx hint allows the control to grow wider and fill up the available space. The gap i hint indents a control to indicate that it is related to the control above it. Setting a title on the container will cause it to have a titled border drawn around it. (See the uilayout library documentation for more information.)

5. Add the controls to the Bindings object.

To create a binding between an attribute and a control, we need to supply three things: the name of the private setting key that is used to store the attribute, the control, and an array of the card faces that need to be redrawn when the attribute changes. Recall that with the $ notation, if we write to a variable called $Effects it will actually modify the setting with the key Effects, so to bind it we just provide this name as a string. The control is just the matching control that will be used to edit the attribute we are binding. The array of card faces is an array of integers, and it will normally be one of the following values:

[0] when this attribute changes, redraw the front face of the component
[1] when this attribute changes, redraw the back face of the component
[0,1] when this attribute changes, redraw both faces of the component

AlAll of our attributes appear on the front face, so:

bindings.add( "Summary", summaryField, [0] );
bindings.add( "Effects", effectField, [0] );
bindings.add( "Foil", foilField, [0] );

6. Tell the DIY which text field (if any) sets the name attribute.

AsAs we learned above, a component's name and comment attributes are built in, so they are not stored in its private settings. Likewise, we don't add bindings for these attributes either. The comment attribute always has its own separate tab, so we don't need to worry about it at all. Strange Eons will manage the name attribute for us, too. We just need to tell it which control is used to edit the name. If we don't set one, the component will still have a name, but the user won't be able to change it directly. This line will tell Strange Eons which control edits the name:

diy.setNameField( nameField );

Of course, we could make our own name attribute and create a text field for it and bind it just the way we did for the attributes. The value of using the built-in name field is that Strange Eons uses it as the component's name when the user saves to a file, adds the component to a deck, exports it, and so on.

7. Add the container to the editor.

At this point we have a container with the needed controls to edit our card, but the container doesn't actually appear in the editor anywhere. We need to call a function on the container to add it as a new tab to the editor window:

container.addToEditor( editor, "Content", null, null, 0 );

The first parameter is the editor to add the container to. That was passed to us from Strange Eons. The second parameter is the label that will be given to the tab. The two null parameters are for event listeners that we don't need to supply because the Bindings object we created will write them for us. The final parameter is the index of our new tab within the tabs that are already there. 0 will make this the first tab. We could add multiple tabs to our editor by making a separate container for each one and adding each container to the editor in turn (all the tabs can share the same Bindings).

8. Create the bindings.

This final step lets the Bindings object know that everything is in place and it can go ahead and link up the controls and private settings. This step is dead easy:

bindings.bind();

What Do We Have So Far?

Phew. That was a lot of little changes. Let's make sure we're both on the same page. Here's what your script should look like at this point:

uselibrary( "diy" );
uselibrary( "ui" );

function create( diy ) {
    diy.cardVersion = 1;
    diy.extensionName = "PlotExample.seext";
    diy.faceStyle = FaceStyle.ONE_FACE;
    diy.frontTemplateKey = "lg-misc-front-sheet";
    
    diy.name = "Shaping Young Minds";
    diy.comment = "";
    $Summary = "Cultists have converted a popular and influential "
        + "professor to their cause.";
    $Effects = "Clue tokens may not be gained by any means at any Miskatonic "
        + "U. location. If the terror level reaches 4, immediately open a "
        + "gate at the Science Building as if a gate burst had occurred there.";
    $Foil = "Instead of having an encounter at the Administration Building, "
        + "you may make a <b>Will< >(-2)</b> check to see the Dean. If you "
        + "pass, discard 5 Clue tokens to convince him that the professor is "
        + "dangerously insane and should be hospitalized. If no gate is open "
        + "on the Science Building, seal it using a token from the doom track.";
}

function createInterface( diy, editor ) {
    var container = new Grid( "fillx" );
    var bindings = new Bindings( editor, diy );

    var nameField = textField( "", 30 );
    var summaryField = textArea( "", 4, 30, true );
    var effectField = textArea( "", 7, 30, true );
    var foilField = textArea( "", 7, 30, true );

    container.place(
        "Title", "split", nameField, "growx, wrap",
        "Summary:", "wrap",
        summaryField, "gap i, growx, wrap",
        "Effects:", "wrap",
        effectField, "gap i, growx, wrap",
        "Foil:", "wrap",
        foilField, "gap i, growx, wrap"
    );
    container.setTitle( "Plot" );

    diy.setNameField( nameField );
    bindings.add( "Summary", summaryField, [0] );
    bindings.add( "Effects", effectField, [0] );
    bindings.add( "Foil", foilField, [0] );

    container.addToEditor( editor, "Content", null, null, 0 );  
    bindings.bind();
}

function createFrontPainter( diy, sheet ) {
}

function paintFront( g, diy, sheet ) {
    sheet.paintTemplateImage( g );
}

function createBackPainter( diy, sheet ) {
}

function paintBack( g, diy, sheet ) {
}

function onClear( diy ) {
    $Summary = "";
    $Effects = "";
    $Foil = "";
}

function onRead( diy, ois ) {
}

function onWrite( diy, oos ) {
}

testDIYScript();

You can run the script to test it out, and you'll see that we've accomplished quite a bit already. We have a card with the right background, all of our controls, and default values for all of our attributes. Thanks to the Bindings object we set up, the edit controls are already filled in with our example. We can even erase the example with the Edit | Clear command. Now we just have to draw the attributes on the card face.

Drawing the Content

To help us draw the card content, we'll use several MarkupBoxes. A MarkupBox can take Strange Eons mark-up text, lay it out, and draw it. We can create a MarkupBox by calling its constructor, but it is usually easier to use the helper function markupBox( sheet ), which will create the box and set it up to work correctly with our component. First, we'll have to add a

uselibrary( "markup" );

to the start of our file. That gives us access to all of the MarkupBox-related goodies. We don't want to create new boxes each time the card is painted, as that would slow painting down. Instead, we'll create the boxes we need just once and reuse them. The createFrontPainter function is provided just for the purpose of setting up the things that we need to paint the front face, so that's where we'll put the code. However, we'll need to have access to the boxes we make in the paintFront function, so we'll store the boxes in global variables. If we put them inside createFrontPainter, they will cease to exist when that function ends.

MarkupBoxes provide a number of options to control alignment (horizontal and vertical), justification, text fitting (whether it will shrink text and/or remove space between lines when the text gets too long), the styles used, and more. Styles can be set for individual mark-up tags, and you can add new tags. Most commonly, though, you'll want to change the default style that is used when no tags are in play. Styles are defined using a TextStyle object, which consists of a collection of keys and values. These are described in the documentation for the markup library, so instead of repeating it all here, let's have an example:

var titleBox, summaryBox, bodyBox;

function createFrontPainter( diy, sheet ) {
    titleBox = markupBox( sheet );
    titleBox.defaultStyle.add(
        FAMILY, FAMILY_AH_LARGE_CARD,
        SIZE, 14
    );
    titleBox.alignment = LAYOUT_CENTER | LAYOUT_MIDDLE;
    
    summaryBox = markupBox( sheet );
    summaryBox.defaultStyle.add(
        POSTURE, POSTURE_OBLIQUE,
        SIZE, 9
    );
    summaryBox.alignment = LAYOUT_CENTER | LAYOUT_MIDDLE;
    summaryBox.textFitting = FIT_BOTH;
    
    bodyBox = markupBox( sheet );
    summaryBox.defaultStyle.add(
        SIZE, 8
    );
    bodyBox.alignment = LAYOUT_CENTER;
    bodyBox.textFitting = FIT_BOTH;
}

We make three boxes: one for the title, one for the summary, and one that will hold both the effects and the foil. We set the font family for the title box to the family used for large cards in Arkham Horror. You could put any string there to name a family of your choice. The alignment property lets you set both the horizontal and vertical alignment of text within the box, and it lets you optionally justify the line ends as well. The summary is always printed as italic text, and although it may not be obvious unless you're a typophile, that is what POSTURE controls.

To draw this content on our card, we need to add a few lines to our front face painter. For each box, we need to tell it what to print and where to print it. The what will come from the appropriate property (setting value). The where will be determined by a Region. There are a few ways to deal with setting up our regions. The proper way is to store them in settings, and then fetch them from those settings as we need them. That way, our new card type can be customized using the same techniques that work for the built-in components. If we want to do things the quick and dirty way, we can hard code the regions in our script. We use the second technique here, but storing the regions in settings isn't much more work. Have a look at the DIY examples to see how.

The Region Editor plug-in is a simple tool designed to help us define region settings visually. It is meant more for fine tuning a region and customizing existing card types than working on whole new cards. If we were using a custom template image, we would probably use the Draw Regions project action or create a new Card Layout file. But the plug-in will also do the job. Run the current version of the script, if you need to, so that you can see our card template in the preview window. Now start the Region Editor. Since we are not creating a real setting, the name in the Region Key box doesn't matter. In the value box, enter a starting value such as 0,0,32,32 and click Write Setting. You'll see an orange and red frame in the upper-left corner of the card. This is your region. Using the compass rose on the left, you can move the region around (change the first two numbers). Using the compass rows on the right, you can change the region's size (change the last two numbers). If you press shift while pressing an arrow, it will move in larger increments. Using the region editor, pick out a region at the top of the card that has room for about one line of large text. Select and copy the region value, then close the region editor. This will be the region for the card title. Just after the line where you defined the mark-up box variables, add a new line that creates a new region using the value you copied:

var titleRegion = new Region( 55, 56, 226, 40 );

Now move down into the paintFront function and add the code needed to draw the title:

g.setPaint( Color.BLACK );
titleBox.markupText = diy.name;
titleBox.drawAsSingleLine( g, titleRegion );

Have a look at the Painting Techniques example to see how to create complex paints and other effects.
Make sure you put it after the line that paints the template, or the text will be painted over by the card background and you won't see it. The first line sets the drawing colour to use. Instead of setting the paint colour on the graphics context, we could have made it part of the default style for the mark-up boxes by adding the appropriate style values. We could also choose a different colour by creating a new Color object. If you know the "Web colour" value, say #ffa734, , you could set it with a line like this: g.setPaint( new Color( 0xffa734 ) );. You could also achieve more complex effects, such as a colour gradient, by using a different sort of Paint object than a Color, but that's beyond the scope of this tutorial.

A MarkupBox will normally break a line of text when it reaches the right edge of the region, starting a new line. While that is often what we want, for titles it is often the case that we want to make the entire title fit on a single line. As you might guess, that's what the last line does. If we'd written draw instead of drawAsSingleLine (and we will in a moment!), we would have gotten the regular line-breaking behaviour.

Try running the script to see the result so far. If you don't like it, adjust the region value as needed to get a good-looking title. Once you are happy with the result, you can start the region editor again and pick out a region for the summary text. Make sure it starts below the title and that there is room for three or four lines of text. Copy the region as before, and add the needed lines to create a region box and draw the summary text:

var summaryRegion = new Region( 55, 100, 226, 80 );
summaryBox.markupText = $Summary;
summaryBox.draw( g, summaryRegion );

Run the script and check on the results. You might find it useful to turn on the Show Regions plug-in, which will highlight all of the regions we draw with a purple box. Make sure the two regions that we have so far don't overlap.

The next step will be to draw the divider decoration that appears between the summary and the effects. First we'll need to get the image. For that we'll use a function from the imageutils library, so be sure to add a line to include it at the start of the script. We'll store the image in another global variable, loading it with the following function call:

var divider = Image.fetchImageResource( "icons/divider.png", true );

We'll have to add a line to the painter to draw it below the summary region. We'll choose a y-coordinate that is a bit below the end of the region, which we can determine by adding the region's y-value and its height. (If you are using my numbers from the code above, that's 100+80). To choose an x-coordinate to draw the image at, we'll use a little math to center the image horizontally:

g.drawImage( divider, (sheet.templateWidth - divider.width)/2, 180, null );

(If you check the documentation for the Graphics class, you'll see that the final parameter to the version of drawImage we are using calls for an ImageObserver. You'll never need one of these when painting a DIY card, so anytime you are asked to provide one just pass in null.)

Now we need to add the code for the last mark-up box. Fire up the region editor and pick out a region that will extend to the bottom of the card. Add code to create the region and draw the text. When you set the mark-up text to be drawn, add some text to add the "Effects:" and "Foil:" labels. You will end up with a final script that looks like this:

uselibrary( "diy" );
uselibrary( "ui" );
uselibrary( "markup" );
uselibrary( "imageutils" );

function create( diy ) {
    diy.cardVersion = 1;
    diy.extensionName = "PlotExample.seext";
    diy.faceStyle = FaceStyle.TWO_FACES;
    diy.frontTemplateKey = "lg-misc-front-sheet";
    
    diy.name = "Shaping Young Minds";
    diy.comment = "";
    $Summary = "Cultists have converted a popular and influential "
        + "professor to their cause.";
    $Effects = "Clue tokens may not be gained by any means at any Miskatonic "
        + "U. location. If the terror level reaches 4, immediately open a "
        + "gate at the Science Building as if a gate burst had occurred there.";
    $Foil = "Instead of having an encounter at the Administration Building, "
        + "you may make a <b>Will< >(-2)</b> check to see the Dean. If you "
        + "pass, discard 5 Clue tokens to convince him that the professor is "
        + "dangerously insane and should be hospitalized. If no gate is open "
        + "on the Science Building, seal it using a token from the doom track.";
}

function createInterface( diy, editor ) {
    var container = new Grid( "fillx" );
    var bindings = new Bindings( editor, diy );

    var nameField = textField( "", 30 );
    var summaryField = textArea( "", 4, 30, true );
    var effectField = textArea( "", 7, 30, true );
    var foilField = textArea( "", 7, 30, true );

    container.place(
        "Title", "split", nameField, "growx, wrap",
        "Summary:", "wrap",
        summaryField, "gap i, growx, wrap",
        "Effects:", "wrap",
        effectField, "gap i, growx, wrap",
        "Foil:", "wrap",
        foilField, "gap i, growx, wrap"
    );
    container.setTitle( "Plot" );

    diy.setNameField( nameField );
    bindings.add( "Summary", summaryField, [0] );
    bindings.add( "Effects", effectField, [0] );
    bindings.add( "Foil", foilField, [0] );

    container.addToEditor( editor, "Content", null, null, 0 );  
    bindings.bind();
}

var titleBox, summaryBox, bodyBox;

var titleRegion = new Region( 55, 56, 226, 40 );
var summaryRegion = new Region( 55, 100, 226, 80 );
var bodyRegion = new Region( 55, 198, 226, 271 );

var divider = Image.fetchImageResource( "icons/divider.png", true );

function createFrontPainter( diy, sheet ) {
    titleBox = markupBox( sheet );
    titleBox.defaultStyle.add(
        FAMILY, FAMILY_AH_LARGE_CARD,
        SIZE, 14
    );
    titleBox.alignment = LAYOUT_CENTER | LAYOUT_MIDDLE;
    
    summaryBox = markupBox( sheet );
    summaryBox.defaultStyle.add(
        POSTURE, POSTURE_OBLIQUE,
        SIZE, 9
    );
    summaryBox.alignment = LAYOUT_CENTER | LAYOUT_MIDDLE;
    summaryBox.textFitting = FIT_BOTH;
    
    bodyBox = markupBox( sheet );
    summaryBox.defaultStyle.add(
        SIZE, 8
    );
    bodyBox.alignment = LAYOUT_CENTER;
    bodyBox.textFitting = FIT_BOTH;
}

function paintFront( g, diy, sheet ) {
    sheet.paintTemplateImage( g );
    g.setPaint( Color.BLACK );
    titleBox.markupText = diy.name;
    titleBox.drawAsSingleLine( g, titleRegion );
    
    summaryBox.markupText = $Summary;
    summaryBox.draw( g, summaryRegion );
    
    g.drawImage( divider, (sheet.templateWidth - divider.width)/2, 180, null );
    
    bodyBox.markupText = "<b>Effects:</b> " + $Effects.trim()
            + "\n\n<b>Foil:</b> " + $Foil;
    bodyBox.draw( g, bodyRegion );
}

function createBackPainter( diy, sheet ) {
}

function paintBack( g, diy, sheet ) {
}

function onClear( diy ) {
    $Summary = "";
    $Effects = "";
    $Foil = "";
}

function onRead( diy, ois ) {
}

function onWrite( diy, oos ) {
}

If you look closely you'll see that I've deleted the testDIYScript() function call. That's because we're ready to save the file and make the whole thing into a plug-in. To do that, right click on the task folder and choose Make Bundle. Strange Eons will package the contents of the task folder up for you and create a plug-in bundle, then let you choose a name. Start by entering PlotExample as the base name. Now this next step is critical. Because we used the empty plug-in task, Strange Eons does not know what kind of plug-in we want, and it defaults to an activated plug-in type, which uses the extension .seplugin. In the rename dialog, we need to change this since we are creating an extension plug-in. To do this, click on the existing .seplugin extension and then edit the name to PlotExample.seext, then click on Rename.

We now have a working plug-in that we can install and use just as any other plug-in. To try it out, we can use a special test mode by right-clicking on the bundle and choosing Test Plug-in. Or we can install it, restart, and try it out that way.

Your Turn

As an exercise, see if you can extend this script to include the back side. Here are some hints: you'll need to change the cards faceStyle setting, add a template key for the back face, and add write a paintBack function. You'll need to paint a total of three images, including the back side template. Check out card-layout.txt to puzzle out the second image you'll need; for the octopus try hunting with the Image Resource Browser plug-in. You can extrapolate from the code for painting the divider to paint the octopus. If you get stuck, look at the finished example for help:

    PlotExample.seext (3 KiB)

More Information

There is a lot of example code available in the Plug-in Authoring Kit that will show you how to accomplish various effects, JavaScript features, Strange Eons features, and more. Don't confine yourself to the diy folder, because there are lots of ideas from other examples that you can transfer to your DIY projects.

Return to Home Page    Send Feedback

June 01, 2009 — Updated April 20, 2010