Templar’s Final Stand — Post-Mortem (JS13k 2024)

The post-mortem of the Templar’s Final Stand, the entry of JS13k 2024 competition.

Leo Kuo
8 min readSep 21, 2024

Templar’s Final Stand is a strategy game inspired by 2048. The player takes on the role of a Templar Knight, trying to survive longer by fighting off enemies while navigating the grid.

Play the game and give me feedback here: https://js13kgames.com/2024/games/templars-final-stand

GitHub repo: https://github.com/leokuo0724/Templar-s-Final-Stand

About the JS13k

The js13k Games competition is a yearly challenge where developers create a game using JavaScript, with a strict limit of 13 kilobytes (After zipped). This tiny size forces participants to think creatively and optimize their code, making every byte count.

In this post-mortem, I’ll share my experience, the challenges I faced, and the lessons learned from developing for js13k 2024.

Ideation

Theme: Triskaidekaphobia

The theme of this year’s competition is triskaidekaphobia (the fear of the number 13). To be honest, that was the first time I had heard about the word!

After researching its meaning and background, I found that the story of the Knights Templar, who were arrested on Friday the 13th, is often cited as one of the origins of triskaidekaphobia. I thought this historical event would be a good fitting inspiration for my game.

Takeaways from Last Year

Last year, I built a game — Mongol March in JS13k competition. (you can find the postmortem here) There were two major takeaways:

  • The game should be easy to control and intuitive for players to understand how to play.
  • Including audio significantly improves the overall experience.

With these lessons in mind, I’m making them my focus for this year’s entry.

Core Game Mechanism

At first, I came up with a rough idea: the game would involve combining numbers, with a penalty if the total equals 13. This concept reminded me of the game 2048, which was a good starting point since its controls are simple and familiar to many players. Afterward, I tried to merge the story of Knights Templar and 2048-like game mechanics, resulting in the following key components:

  • Actions:
    In the game, players can move up, down, left or right. The controls are simple but every move has consequences. These actions results in attacking enemies, being attacked, equipping items or upgrading items.
  • Rewards:
    - Upgrade items: Combining identical items upgrades them, making them stronger.
    - Equip items: Equipping items grants additional power.
  • Punishment:
    Equipping items increases your power but also adds weight. When the weight reaches 13, every move will cause damage to the player.
  • Enemies:
    After every 13th move (i.e., at 13, 26, 39…), “elite” enemies will spawn. These powerful enemies have special abilities, such as penetrating shields or attacking in a wider range. Players must carefully strategize to overcome these challenges.

Basic Project Structure

I built this year’s project on the foundation of last year’s entry, using Kontra.js as the game engine and Vite as the bundler. Naturally, I also used RoadRoller to compress the JS code. The Vite settings were largely adapted from roblouie’s js13k-typescript-starter project.

However, I realized that it’s a mistake that I didn’t use Closure Compiler to minified the code last year. I encountered some issues and ended up skipping the minification before using RoadRoller. This time, I used Terser to minified and uglify JS code before running RoadRoller, and the results were impressive: a compression rate of over 17.7% reducing the size from 13,680 bytes to 11,256 bytes. It gave me more room to add additional game content!

Graphics

This time, I stuck with vector art design, which is my strength. The difference is that:

Last year’s approach: generate SVG image in html -> get the image element and draw via drawImage function (Kontra implements that in Sprite class)

const SVG_DATA = {
shield: 'width="27.6" height="27.6" viewBox="0 0 27.6 27.6"><circle cx="13.8" cy="13.8" r="13.8" fill="#AE5D40"/><path fill="#79444A" d="M12 10h3v8h-3z"/><path fill="#D1B187" d="M12 12h3v3l-2 1h-3z"/></svg>',
// store all svg data
};

// generate image elements in html
const imgContainer = document.getElementById("imgs");
Object.entries(SVG_DATA).forEach(([id, data]) => {
const encoded = encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" ${data}`
);
imgContainer?.insertAdjacentHTML(
"beforeend", `<img id="${id}" src="data:image/svg+xml;utf8, ${encoded}" />`
);
});

This year’s approach: control canvas context path to render every shapes. For example, there is a SVG shape:

<polygon points="11.54 74 0 73.99 0 77.21 4.16 77.22 4.15 86.83 7.38 86.83 7.38 77.22 11.53 77.22 11.54 74" fill="#ae5d40"/>

The first thing I did was round the numbers to save space. Then, I used CanvasRenderingContext2D to draw the shape. Of course, I wrote scripts to handle both of these steps. Here is my drawPolygon function:

function drawPolygon(
ctx: CanvasRenderingContext2D,
points: string,
fill: string,
offsetX?: number,
offsetY?: number
) {
const pointArray = points.split(" ").reduce((acc, cur, i, arr) => {
if (i % 2 === 0) {
acc.push({
x: parseFloat(cur),
y: parseFloat(arr[i + 1]),
});
}
return acc;
}, [] as { x: number; y: number }[]);

ctx.beginPath();

ctx.moveTo(
pointArray[0].x + (offsetX ?? 0),
pointArray[0].y + (offsetY ?? 0)
);
for (let i = 1; i < pointArray.length; i++) {
ctx.lineTo(
pointArray[i].x + (offsetX ?? 0),
pointArray[i].y + (offsetY ?? 0)
);
}
ctx.closePath();

ctx.fillStyle = fill;
ctx.fill();
ctx.closePath();
}

Results

Using context 2D to directly draw shapes allows for convenient control over each individual shape. Additionally, this method is more suitable for drawing vector shapes, as it maintains quality without distortion when scaling. Moreover, by avoiding the use of drawImage, you can leverage Kontra’s tree shaking mechanism to remove unnecessary code, further reducing the overall size!

Audio

Last year, I didn’t include any audio in my game because it’s not my strong suit. However, based on feedback, I realized that adding audio is essential for enhancing the overall game experience!

Therefore, I incorporated ZzFXM early in the development process to evaluate the file size.For the sound effects, thanks to the ZzFX generator, I experimented with a few randomly generated sounds and found them suitable for the game. The results were quite good and indeed enhanced the overall game experience.

Next came the part I’m least familiar with: composing the main theme music. For this, I initially leveraged AI by generating some music for inspiration using keywords in Suno. Then, I tried creating the music in the ZzFXM tracker.

ZzFXM tracker

Polish time!

After finishing the core gameplay and essential elements of the game. It was time to polish.

Gameplay — classes

The first thing I want to improve is gameplay. In the initial version of the game, the strategy seemed rather static, mainly revolving around collecting weapons, equipment , and potions, and attacking enemies at the right moment.

However, I wanted to provide players with a different experience, so I ultimately chose to introduce various “classes”, each corresponding to a different strategy, allowing players to enjoy diverse gameplay.

  • Knight: The most basic version, focusing on weapon synthesis to boost attack power.
  • Wizard: Emphasizes avoiding enemies and collecting or synthesizing potions to attack enemies.
  • Defender: Focuses on counterattacks, aiming to collect as many shields as possible and encouraging being attack by enemies.
Three different classes offer distinct gameplay strategies
The Wizard can attack all enemies with potions but is physically fragile
The Defender should relay on counterattacks

Game Balance

After adding more classes, game balance became crucial. The goal is to ensure that the difficulty of all three classes is consistent. This involved extensive playtesting and adjustments based on feedback from my friends.

In addition to the different initial stats for each class, the appearance rates of items and the values of synthesized items also very depending on the class.

Story & prologue

To enhance immersion in the game, I added an introductory background stroy at the beginning, allowing players to understand that they are playing as a member of the Knights Templar, needing to evade the soldiers pursuing them under King Philip IV.

Enemy characters

During the playtesting process, I received feedback that it was sometimes difficult to distinguish between elite enemies and regular ones at a glance (despite textual descriptions), which led to incorrect movements.

To address this, I used different weapons to differentiate enemies. For example, an axe represents area attacks, while a spear represents penetrating attacks. This makes the distinctions much clearer.

Normal enemy and different elite enemies

Controls

The final aspect of polishing was the controls. I implemented audio mute/unmute functionality and speed control. The speed control allows for adjustments to the animation timing. Since my animations are all created using a custom tween function, it’s easy to control the speed.

Speed control on the top left corner

Compression

The final hurdle before uploading was compression. After all the polishing and improvements, I excitedly checked the file size — only to find that my entry had exceeded the 13KB limit. It wasn’t by much, but it had grown to around 13,600 bytes.

So, I examined the JavaScript files before compression with RoadRoller to see where further reductions could be made. Ultimately, I discovered two ares where the strings were not being uglified:

  1. Property names in classes(objects)
  2. Keys of enums

By shortening the names in these two parts of the code, I successfully kept the file size under 13KB! While this came at the cost of readability, it proved to be an effective compression strategy.

For example, there is a “main” prop defined in the class, it won’t be uglified by Terser.

As for whether Terser provides a configuration option to compress properties within objects, that’s something I’ll need to research further.

Summary

It’s worth celebrating that I’ve completed my second challenge of creating a game for js13k! I believe the most interesting part of the competition is resource management. The 13KB limit forces us to prioritize our ideas, encouraging creativity and innovation within constraints.

Each decision — from graphics to audio — requires careful consideration, making the final product not just a game, but a testament to efficient design and strategic thinking. This time, I’m particularly satisfied with completing the audio. However, I believe there’s still room for improvement in the game’s intuitiveness. I will take this as my challenge for next year.

Finally, I’d like to thank Andrzej Mazur once again for organizing this event!

--

--

Leo Kuo

Indie Game Developer | Frontend Developer | iOS Developer | UIUX