Character sheet creation guide
This guide will introduce you to the basics of creating a Marketplace attribute template (or "character sheet" as it is broadly named nowadays). For simplicity, I will refer to them as MATs or simply templates, since they can be used for much more than just character sheets.
It is recommended to know the basics of HTML before you look into creating a template, and a minimal understanding of what CSS is would also be beneficial. Rather than try to be a crash course in HTML and CSS, this guide focuses on the structure and functionality of MATs and dives into the more advanced capabilities offered by the Blade syntax, such as conditional operations and loops. It will be most beneficial to those who can already build simple web pages but are not familiar with programming languages.
The basics
First, let’s review what MATs can do, especially in contrast to the attribute templates you can create directly on your campaign. The creation interface essentially comes in five parts; in no particular order, these are:
Attributes. Just like on your campaign, you can define a series of attribute names and types, and optionally their default values. Those will be added to any entity you apply the template to. However, unlike campaign attribute templates, you cannot apply multiple MATs to an entity, because they control how the Attributes page is formatted and are mutually exclusive. You can also make some attributes hidden, meaning they won’t show up in the Attributes tab when editing the entity, unless the user specifically asks to see them. This is great to avoid cluttering the interface if you use attributes for behind-the-scenes calculations.
CSS. Your MAT has its own dedicated stylesheet that is loaded exclusively on the Attributes page of the entities it is applied to (and other places a character sheet might show up, like a post or dashboard widget). Otherwise, this works similarly to the stylesheets you can create in the Theming page of your campaign’s settings.
HTML. The contents of this field will completely replace the attribute listing that you normally see in the Attributes page. The markup you enter here will define the layout of the main content area, including which attributes are displayed in what order and how. Even more importantly, it allows a limited set of logic operations and "control structures" such as conditional statements (if, else if, else) and loops. We’ll talk a lot about those later.
JavaScript. Kanka doesn’t normally allow users to add custom JavaScript programs to their campaigns, but MATs are the exception to that rule. This opens up vast possibilities that can’t be covered in a tutorial, but JavaScript in character sheets contains various examples.
Translations. This section allows you to define translated text for use in your MAT. This (as well as a different approach) is discussed in Localizing character sheets.
Before we get ahead of ourselves, let’s quickly run through some general tips and caveats for the simpler aspects of creating a MAT:
A good thing to know as you start developing your plugin is that you can install it to your campaign while it’s still a draft. Then apply it to a test entity and your changes will automatically be synced to your campaign – just refresh the page to see the updated results. If you are testing a new draft after previously releasing the plugin, you will need to apply the update once in your campaign’s Plugins page (but only once per public release, not after every change to a given draft).
Note however that the default attributes will not be reset on the entity if they change in the MAT. For those changes, it’s best to delete all attributes and reapply the template.
The Marketplace editor will warn you if your plugin contains invalid variable names and some other errors. Most errors however will be reported on the entity itself. Generally these will tell you roughly what’s wrong, and the line number that caused it, though sometimes the root cause of a crash lies earlier in the code.
A simple example
Without going into the details of HTML and CSS, here is a simple example of how the three main fields come together.
Attributes
As you can see, charAge
isn’t displayed at all even if the entity defines its value, because it is not part of our template’s HTML. If the entity has a different value for displayName
than the default "Ziggy", that value will be displayed instead.
Variables
All right, so let’s start talking about the interesting stuff. First, you’ll need to know exactly how to handle variables to manipulate your attributes as needed. MATs use the Blade syntax, which is a limited and simplified layer on top of the PHP programming language. Using that syntax, Kanka provides you with one variable per attribute in your template, and they look like this: $displayName
.
Note that you cannot create additional variables in your code for behind-the-scenes operations. If you need a variable, you must create a corresponding attribute. This is where hidden attributes can come in handy.
(This is not technically true, but what is intended, so I will stick to that assumption for the most part.)
Those variables are used in three ways, or formats:
Use this syntax: {{ $displayName }}
to display the attribute as plain text, meaning that any HTML in the value will be displayed raw rather than interpreted as markup. Example:
Result: <em>Not italicized</em>
Meta Variables
As stated in the plugin helper page, Kanka provides several variables about the environment that your MAT can use to make certain decisions when the page is rendered or show information beyond its attributes:
Tags in particular could be very useful as configuration options to style your sheet differently if the entity belongs to a certain category, hide or add entire sections that are marked as relevant or not, or even display completely different templates using a single plugin based on tags.
Note that many of these fields are optional and may return a null
value, so it is safer to test them with @isset
or @empty
before doing anything else with them. See Testing variables and what happens when using @if
on a missing attribute (and in this case, a null variable).
Working with Blade
Here are a few more general tips regarding Blade syntax:
You can write comments in the HTML field that will not show up on the final page, to help you remember what everything does if you come back to edit your MAT months later. The Blade syntax for comments is
{{-- For my eyes only --}}
. HTML comments<!-- like this -->
can also be used if you prefer, and will be visible when inspecting the page source in the browser.If you have experience with programming languages but not templating engines, keep in mind that you don’t need functions like
echo()
orprint()
to display HTML. Rather, everything that isn’t part of a Blade statement is meant to be printed. HTML is the default, and programming logic the exception.Spaces aren’t necessary when displaying variables (i.e.
{{$displayName}}
will work as well as{{ $displayName }}
), but are necessary before @ statements. For example,@if(1 == 1)Show this@endif
will fail but@if(1 == 1)Show this @endif
will work.Now that JavaScript is allowed in MATs, you don’t actually have to work with Blade at all. If you are more familiar with JS and want to leverage its fuller capabilities, JavaScript in character sheets suggests a way to export all the variables that Kanka provides in Blade to JS and build your sheet using the latter. But for the most part, this guide assumes you won’t use JavaScript.
And now, let’s dive into the various statements available to us.
Testing variables
Since your MAT will be used by other people and they can edit their entities’ attributes anytime, one important thing you will need to do is ensure the validity of not only attribute values, but the attributes themselves. As developers say, never trust user input. What happens if the attribute has been deleted or changed to the wrong type? What if it’s present but empty, and you need its value to proceed? What if it’s there but doesn’t match one of the values you were expecting? Since you’re not building an actual application that could delete or corrupt anyone’s data, you don’t have to be super diligent about this, but this section explains the problems you can expect and ways to mitigate them.
Below, I will show three ways to test variables and the results they give in different scenarios. To represent these scenarios, we will use the following variables/attribute values:
$Set
is a standard, numeric or multiline attribute with a value set.$Empty
is an attribute with no value set. The program knows it exists, but it does not currently have a value.$Missing
is an attribute that doesn’t exist at all on the entity. Perhaps it was deleted or renamed by a user, or there is a typo in your code, or you simply forgot to create it. Meta variables based on optional fields, like a character’s age, may also fall under this category.$Checkbox
is a checkbox attribute, which can be on or off.$SectionName
is a section attribute, and exists without an associated value.$Array
is a collection of variables, such as the Kanka-provided$_tags
,$_abilities
,$_character_traits
and$_character_appearances
. Those cannot be displayed directly with the{{ $Array }}
syntax because they are only containers for other values, so you will often want to test them first, then use them in a @foreach loop. Note that only Character entities have traits and appearances. On other entity types,$_character_traits
and$_character_appearances
will behave like$Missing
.
Feel free to recreate those attributes in a test entity to reproduce or adapt the examples that follow and see for yourself.
Each testing method has three possible outcomes:
true (the condition is considered positive and executes the associated code);
false (the condition is considered negative and the associated code is ignored, skipping to the @else clause if any);
invalid (the condition fails to execute and triggers an error).
The following table summarizes the results detailed in the next paragraphs:
@isset
One of the most reliable tests, @isset will tell you if an attribute exists, and will not throw an error otherwise, but doesn’t care whether it has a value. It returns true
for $Set
, $Empty
and $SectionName
, and false
for $Missing
. It also returns true
for $Checkbox
regardless of whether it is ticked. So it’s a good choice if you only want to know whether the attribute exists on the entity.
On the other hand, it’s useless to test for the presence of tags and character traits or appearances since those arrays exist even when no tags, traits or appearances have been set. Of course, you can test a specific variable inside an array if you know what name to expect, for example:
@empty
@empty takes a nearly opposite approach and will give a positive result when an attribute has no value, including if it’s completely absent. It returns true
for $Missing
, $Empty
and $SectionName
(which cannot have a value), and false
for $Set
. It also returns true
for $Checkbox
if unticked, or false
if ticked. Useful when you want to do something when a value is not set, for example display a notice to the user. However, it doesn’t distinguish missing attributes from merely empty values.
It’s also the safest approach when dealing with arrays like $_character_traits
and $_character_appearances
, since it won’t cause errors if the array doesn’t exist at all, which can happen if your template is used on entities other than Characters.
@if
@if can also be used to test variables without its typical "equal to / greater than / lower than" comparison operators, but works a little more implicitly. It only returns true
for $Set
and a ticked $Checkbox
, and false
for $Empty
, $SectionName
(which never has a value), and an unticked $Checkbox
. That makes it more or less the opposite of @empty, meaning you can use it to do something when a value is present.
However, you’ll run into problems with @if when the attribute does not exist: Error: Undefined variable $Missing
will crash your template altogether. Therefore, if you want to perform an action only if an attribute has a value, the safest approach is to use either @isset or @empty before @if:
The same can also happen if you are testing for character traits or appearances and the entity is not a Character. If you are checking a specific tag, trait or appearance name, you will instead see Error: Undefined array key "name"
, and you could mitigate that with:
Another quirk can become apparent when using @if to test number attributes. Without going into too much detail, it doesn’t actually check the existence of a value, but rather whether an expression is "truthy" or "falsy". In a number attribute, 0
is a perfectly reasonable value, but it’s considered falsy in Blade (think of it as 0 = OFF, 1+ = ON). So if you ask @if($AttributeWhoseValueIs0) A @else B @endif
, you will get B
. For this reason, @empty may be a better choice when dealing when numbers.
Bonus round: default values using the ternary operator
There is also another trick you can use when you want to revert to a default value when an attribute is empty. Say you have a ChallengeLevel
attribute with only five possible values ranging from Trivial to Impossible, and you want Trivial to be the default when a user doesn’t specify. The following syntax basically means "Show the value of $ChallengeLevel
if it’s set, or 'Trivial' if it’s not":
In many cases, this is way faster than testing each attribute and running different code blocks based on their status. It’s especially convenient if you want to use attributes in calculations and want to avoid running into errors due to missing values, as it lets you define a safety net without a bunch of wordy conditions. For example, the following calculates a Perception score with a default of 0 for each possible bonus involved, all on a single line:
Note however that it will still generate an error when attributes are missing altogether, so you may want to test them with @isset first.
Comparisons, conditions and loops
Now that we know how to ensure that our attributes are set as expected, let’s continue with a detailed look at our various options for conditional logic and loops. We’ve already seen a bit of @if in action, which is likely the statement you will use the most. In addition to just testing if a variable is "true", @if is frequently used to compare a variable’s value with the ones we expect, using the operators that follow. However, these are not specific to @if and you will also see them in various examples below.
Comparison operators
$var == "text"
: true if the value matches "text" exactly. Note that text strings require quotation marks (single or double).$var != "text"
: true if the value does not match "text" exactly.$var == 5
: true if the value is 5. Note that numbers do not use quotation marks."5"
is not equal to5
.$var < 5
: true if the value is lower than 5.$var > 5
: true if the value is greater than 5.$var <= 5
: true if the value is 5 or less.$var >= 5
: true if the value is 5 or more.$var == false
or!$var
: true if the value is empty but the attribute exists. This lets you test for a missing value, similarly to @empty, but will still throw an error if the attribute simply does not exist. Note that as discussed above,0
in a number attribute is falsy, so@if(!$var) Not set @endif
will returnNot set
when the attributevar
is in fact explicitly set to 0.
What’s more, you can combine comparisons with AND
and OR
(or respectively &&
and ||
if you prefer), as well as group them with parentheses:
@if ($a > $b AND $b > $c)
, or@if ($a > $b && $b > $c)
, will only be true if both conditions are met;@if ($a > $b OR $b > $c)
, or@if ($a > $b || $b > $c)
, will be true if either condition is met, or both;@if (($a > $b OR $b > $c) && ($d > $c || $c > $e))
, will be true if either condition is met in both groups of comparisons delimited by parentheses.
All of those options give you vast possibilities to style things based on their value, hide or show entire sections depending on whether certain attributes are present, etc.
@if, @elseif, @else, @endif
When it comes to @if specifically, you can make a simple, one-off check:
You can respond to two possibilities of a binary state with @else:
And you can handle an arbitrary number of possible values with @elseif:
@switch, @case, @break, @default, @endswitch
@switch is essentially the same as a series of @if and @elseif written differently. But where @if can test a whole bunch of comparisons at once, @switch tests the value of a single variable (as with ==
) and does something based on what it matches:
If a case is matched, the associated code will be executed. If that block ends with
@break
, the rest of the switch will be ignored. But if there is no@break
, every subsequent case will also be executed (until a@break
happens, at least)!If no case is matched, the optional
@default
block will be executed, if provided.You can write several empty cases in a row to give them a shared block to execute:
@for, @continue, @endfor
Warning: The @for (
syntax with a space is broken at the time of writing and does not produce a helpful error message. Instead, use @for(
without a space.
@for produces a loop, which means it repeats a set of operations a certain number of times – typically, incrementing a variable to track your progress and determine when to stop. It takes three arguments:
An expression to execute before starting the loop;
A condition that determines whether the current iteration should cause the code block’s execution (if not, the loop ends).
An expression to execute after each iteration.
Thus, a typical @for loop looks like this:
Here, an "iterator" ($i
) is created to keep track of how many times the loop has executed, starting at zero times. $i < 10
means it runs until $i
reaches a value of 9, inclusively (when $i
is not under 10, the loop stops). And $i++
increments the value of our tracker by 1 after each iteration. Therefore, the text will be displayed 10 times with an increasing number from 0 to 9. The last iteration will see that $i
is not lower than 10 and end the loop.
For MATs, @for has very limited uses since we don’t have access to advanced functions to manipulate arrays of variables and determine when to stop processing. But for the sake of giving an example, suppose you had an attribute named RowsToShow
and you wanted to use that to determine how many empty lines to provide for note-taking in a template meant for printing, you could then do:
Another possible use is if you have attributes with similar names, for example an organization with a fixed number of members specified in attributes Member1
, Member2
and so on. You could then use concatenation in the variable names to run the same logic on all attributes instead of targeting each one’s name specifically:
Here, the ${}
syntax allows us to combine multiple elements into a single variable name. In this case, we have a string "Member"
concatenated using the .
operator with a variable $i
, resulting in Member1 through Member5 as the loop progresses.
For a more complex example: say you wanted to highlight a character’s current level in a list, but also the ones immediately before and after, based on a Level
attribute. You could print all 20 levels in one @for loop, with an @if condition to give a different CSS class (either "special" or "normal") to the desired levels:
Lastly, you can use the @continue statement, which lets you tell a loop to not do anything on the current iteration, but continue to cycle through. So if we wanted to handle a list kind of like above, but instead show everything except the value of an attribute like Path
, it could look like this:
@foreach, @continue, @endforeach
@foreach was implemented with Kanka 1.37 and makes it easier to loop through discrete sets of attributes than with @for. Instead of relying on a single incrementing variable, @foreach iterates over arrays – in other words, it takes a collection of variables and does something for each of them, stopping when there are no more variables to process. Currently, you can use it with the meta variables $_tags
, $_abilities
(or its child array $_abilities['tags']
), $_character_traits
and $_character_appearances
– or with $attributes
to go over every attribute indiscriminately.
Character traits
Let’s try a simple example and say we want to display all of the character’s traits in our character sheet. We don’t know their names, nor even how many there are, so @foreach is exactly the tool for this job.
Note that @foreach will not fail or throw errors if the array is empty (for example, if a character has no traits). It will simply execute 0 times. However, since traits and appearances only exist on Character entities, it can be prudent to test those two with @isset first if you suspect your template could be used on other entity types, since it will throw an error if the array is missing entirely (because other entity types don’t generate it at all).
So the way this works, if you look at the parentheses on line 2, is that you first state which array you are working with, then define the variable names by which you want to refer to each pair of key-value. It’s good practice to pick names that make it clear what information you are working with, but I could have used anything instead of "trait" and "value".
And just like with @for, we can use the @continue directive to skip undesirable items. Here, we want to omit traits that have no text but weren’t deleted from the entity. The rest of the code block is only executed if that condition is not met, listing all traits that do have a value.
Abilities
Since $_abilities
contains both strings and arrays, and some may be null, iterating through it is a little trickier. The following example shows how to set up a convenient loop and display the most useful info, so you can just copy the parts you need.
Bonus lap: the $loop variable
Inside @foreach loops, Blade provides a handy $loop variable that gives you access to various information about the overall loop and the current iteration, such as how many items are in the array, whether this is the first or last iteration, or an odd- or even-numbered iteration, etc. This can be very useful for lists or table rows with alternating styles, or cases where you need different logic at the start or end of a section, and the Blade documentation linked above describes each available property.
In the first character trait example above, we used {{ $loop -> iteration }}
on line 6 to preface each character trait with an index number.
Though not present in the Blade documentation, the spaces surrounding the arrow are necessary when outputting a property because of Kanka’s checks on variable names (hyphens are not allowed in them, and without a space, it doesn’t know where the variable name ends). They are not necessary in statements like @if ($loop->first)
, but it may be a good habit to include them anyway to avoid oversights.
In the second example, we used $loop->last
on line 21 to determine whether to add a comma or a period after each tag. We also used $ability = $abilities[$loop->index]
to make $ability
a shorthand for "the current ability’s sub-properties". This is because the $_abilities
array is structured as follows:
$_abilities
0
name
etc.
1
name
etc.
If you only loop through the first level with @foreach($_abilities as $ability)
, $ability
will equal 0, 1, etc. What you want is $ability[0]["name"]
and so on, and the loop index matches that array index on every iteration. By reassigning it to $ability
on each iteration, we’re simply making it more convenient for us to refer to those properties as just $ability["name"]
in the rest of our code.
Troubleshooting, tips and things of note
Attributes
Pay attention to the naming limitations listed on the MAT helper page to avoid errors, especially when it comes to special characters, spaces and capital letters. That page also explains how to deal with numeric attributes with limited ranges of accepted values and the live editing feature, which are themselves explained in the attribute documentation, so I won’t go over those in this guide.
One thing the helper page actually gets partly wrong is spaces in attribute names. You can use them just fine in the attributes themselves, as long as you omit them in Blade code. So for example, you would refer to
Attribute one
as$Attributeone
. So you can have more human-readable attribute names in your templates!Similarly, hyphens in attribute names won’t cause issues if you only use them as part of live attributes, e.g.
@liveAttribute('Attribute-One')
is valid. However,$Attribute-One
is indeed an invalid variable name in PHP and you won’t be able to save your plugin with it.
CSS
I recommend wrapping your template in a container with a unique ID or class and prefacing every CSS declaration with that ID/class to avoid conflicts with other Marketplace themes or campaign CSS. For example, use
#my-awesome-template table {}
rather than justtable {}
. Not only can other stylesheets mess with your sheet if your selectors aren’t specific enough, but since users have the option to load character sheets in entity posts and dashboard widgets, your stylesheet has the potential to affect various, unpredictable external elements.As usual, to import fonts from services like Google Fonts, your
@import
rules need to be at the top of the stylesheet. Since every character sheet has a dedicated stylesheet, you don’t have to worry about conflicts with other themes or templates; just make sure to put them at the top of the CSS field.Kanka dynamically injects custom properties for each attribute at the bottom of the stylesheet when serving an Attributes page (for example, if you have an attribute named
clan_color
, it will inject:root { --attribute-clan-color: value;}
with the value set on the specific entity). You can leverage this to let a user customize their character sheet’s aesthetics with parameters such as colors or background image sources, though I recommend also specifying a fallback value. In particular, note that you cannot use a syntax likebackground: url(var(--attribute-image));
— var() cannot be parsed inside url(), so the attribute value must correspond to the entire CSS property (url(http...)
).
HTML & Blade
When you save a MAT, your HTML field is analyzed by a Blade filter. If it finds something it doesn’t like, it will rip it out without warning and save without the offending code. So make a backup of your code every time you’re about to save to avoid losing large chunks of it without recourse (and to help you track down what exactly got ripped out so you can try to fix it).
Any attributes that you don’t specifically output in the HTML field will not appear at all on Attributes pages. This is intentional, so that you can use attributes purely for calculations without displaying them. If you don’t see an attribute you think should be there, you probably misspelled its name, or something in your HTML or CSS is hiding it from view.
Why does my attribute’s value show HTML tags instead of rendering them?
If you display a Multiline attribute, remember to use this syntax: {!! $attribute !!}
to render HTML formatting. With the regular brackets-only syntax, even line breaks in the text field will be turned into plain-text HTML tags.
Why do my live attributes show their code like
@liveAttribute("Origin")
instead of rendering their value?
Only single quotes '
are accepted with the @liveAttribute()
method.
See also
This guide about using different layouts based on a container’s width, useful to account for cases where your sheet is displayed in a dashboard widget or to set breakpoints for your various layouts using more specific measurements than total window size.
My guide to localizing character sheets using either the official
@i18n()
method or my custom approach.JavaScript in character sheets for examples of how you can use JS in character sheets to make them more interactive or supplement Blade.
Conclusion
And that’s about all I have for now! If you have any questions, feel free to ask me on the Kanka Discord.
A lot of time and effort went into creating this guide and maintaining it up to date. If it helped you, please consider supporting my work with a tip on my Ko-fi page =) I am also sometimes available for commissions to help directly with your templates or CSS.
Last updated