Posted September 26, 2021 by Sean
Some folks have shown interest in how this game works, so I thought I'd do a quick writeup of how you could go about adding content to it! Almost all of the ingame content is loaded at runtime from a couple text files, which makes adding things pretty simple once you know the basics.
To start, you'll need a downloaded copy of the game. The easiest ways to download it are to use the itch app, or to clone the GitHub repo and build it from source. Building it from source has some benefits but requires more setup, so let's stick with the itch version for now.
Once downloaded in the app, select the gear icon, then "Manage", then "Open folder in explorer" to find the files:
We can ignore most of these: Everything we need to modify is under /assets
.
The relevant files here are:
textures/
: A folder with all the in-game imagesassets.txt
: A list of all the files loaded by the gamecards.txt
, obstacles.txt
, levels.txt
: All of the cards, enemies/encounters, and level generation (these are labelled .txt
, but actually contain javascript)To add a custom hero to the party, we'll first need to draw them. For demonstration, I'm just going to grab the existing peach character from the intro and make a couple quick edits so they're facing the right direction and have a weapon:
...good enough.
I'm going to name this peach_hero.png
, and put it under assets/textures/
. To let the game load the new image, we add a line to assets/assets.txt
:
textures/cards/Priest.png
textures/cards/Rogue.png
+ textures/peach_hero.png
Next, we need to add them to the party. The party is created in assets/levels.txt
, near the top of the return
. I'm going to put them in after the apple rogue:
scene.addParty({
name: 'Rogue',
spr: 'apple',
maxHealth: 2,
damage: 2,
});
+ scene.addParty({
+ name: 'Peach',
+ spr: 'peach_hero',
+ maxHealth: 4,
+ damage: 99,
+ });
Now when we start the game...
...there she is!
We've added a custom hero, so let's make a new card to go with them next.
All of the cards are defined in assets/cards.txt
. Since I drew the hero with a spear, let's add a card that references that:
'Heal Knight': heal('Knight'),
'Heal Priest': heal('Priest'),
+ Pierce: {
+ sprite: 'Slash',
+ description: 'Peach does 1 damage to two enemies',
+ canPlay(scene) {
+ return scene.enemy && scene.alive('Peach');
+ },
+ effect(scene) {
+ scene.obstacles[0].damage(1);
+ if (scene.obstacles[1]) {
+ scene.obstacles[1].damage(1);
+ }
+ scene.sfx('sfx1');
+ },
+ },
There's a few things to note here:
sprite
is setting the icon for the carddescription
is setting the tooltip text shown on hovercanPlay
is preventing the player from using the card if our hero isn't alive to use it, or if there's no enemy for it to targeteffect
is implementing the card: it damages the first enemy, also damages the second enemy (if there is one), and plays a sound effect to go with it.To get the card ingame, I'm just going to give it to the player in levels.txt
right after the party is created:
scene.addParty({
name: 'Peach',
spr: 'peach_hero',
maxHealth: 4,
damage: 99,
});
+ scene.addCard('Pierce');
Now when we restart the game...
...our new card will be available to use in the intro!
If we wanted to give our card a custom icon instead of re-using one of the existing ones, we could add a new asset and replace the sprite
with its name.
So what about enemies?
Like the custom hero, we're going to want add a new drawing for this, and add it to assets/textures
and assets/assets.txt
.
textures/cards/Rogue.png
textures/peach_hero.png
+ textures/eggnemy.png
Next we'll add the enemy to obstacles.txt
:
bat: {
name: 'Eyebat',
health: 1,
damage: 1,
},
+ eggnemy: {
+ name: 'Eggnemy',
+ health: 2,
+ damage: 1,
+ start(scene) {
+ scene.log('"Let\'s get cracking!"');
+ },
+ interact(scene) {
+ scene.log('*crack*');
+ },
+ end(scene) {
+ scene.log('"All I wanted was to be an omelette..."');
+ },
+ },
This definition looks a bit like the party member, but is a bit more involved: start
, interact
, and end
are functions that run when the player encounters the enemy, when the enemy is hit with "Advance", and when the enemy is defeated.
I've just made it add some flavour text for the action log, but this is where all of the encounter logic lives. Try checking out some of the other obstacles for more complex examples!
If we want this obstacle to show up ingame, we'll need to modify levels.txt
again. I'm going to replace the list of basic enemies with this new one so we're guaranteed to run into it in the first level:
- const basic = [['bat'], ['rat_small']];
+ const basic = [['eggnemy']];
Now we can restart the game, embark into the dungeon...
...and fight our enemy!
You might notice when going through the camp here that the peach we added earlier doesn't have a card icon: You'll need to edit the "Reorder Party" card in cards.txt
to add one.
This covered all the basics, but cards.txt
, obstacles.txt
, and levels.txt
all have comments at the top explaining their formats, and cards.txt
also lists out a bunch of the scene functions that handle more complex effects and interactions. Some of the more important bits to note:
obstacles.txt
includes an array of cards near the top called cardsAll
: loot is pulled from this list, so make sure to add your cards here if you want them to show up in chests!levels.txt
includes multiple different "pools" near the top which group obstacles by type for the level generation (e.g. basic enemies, restore points, minibosses). You can customize the level generation itself, but the easiest way to make obstacles show up ingame is to add them to the relevant pool.cards.txt
just like any other cards would be.Feel free to reach out if you try playing around with the files and have any questions!