Wildermyth Legacy
Heroes of the Yondering Lands
Wildermyth is a wonderful procedural narrative game where you recruit randomly generated characters and then watch them change over their lives. They start as farmers and grow into famed warriors. They get married, confront their pasts, can lose their limbs or life in battle, and go through major transformations. The game's common praise is that players become more attached to the procedurally generated and storied heroes than they do to fully authored characters in linear games. Characters that live to the end of a campaign (essentially a complete story) go to live in your "player legacy" and can be re-recruited for future stories. This adds an interesting Pokemon aspect to the game. You're not just catching new heroes, but you're watching them evolve in both their skills and their histories and relationships.
I was really looking for a game that had good characters, a narrative focus, and RPG elements and happened on Wildermyth by finding a random glowing review of it through my RSS feed. (Another benefit I've reaped from going open source). I've had a blast playing it both alone and as a multiplayer game with friends (with two copies of the game and remote play we've done 4 player campaigns where two people host and connect, and the other two each remote play with one of the hosts. Then each person shares controls/characters with just one other person. It's even better with more copies of the game, as Wildermyth let's you assign characters to specific players).
As important events happen to your characters, entries are added to their history. You can edit those history entries, but they are limited to 700 characters and you can't add arbitrary entries. As we had fun stories 'emerge', or relationships develop outside the game's knowledge (say a redemption arc, or an ironic series of events), there isn't a built in way to document those wider stories.
After a session with friends, I'd find I'd want to review the characters, or that I had a question about one of the stories from the session. I wanted to spend more time in that world, without having to load the game up. I realized I wanted a 'companion app' that would let me view my collection of heroes on my phone, and that would let me create and store additional info about those characters. The game's save files are in json, and the legacy has a built in 'character export' that let's you export character data as well as separate png files for both their body and head.
Design Decisions
I set out with several design goals:
-
Access the app on both desktop and mobile
-
Client side only (at least initially)
-
Ability to add additional info to the characters
-
No need to re-import the data every time you viewed the app
As I've spoken about in my blog on quest command, I generally hate doing UI work as it always seems the slowest process of any project I work on. The best UI experience I've had is with HTML and CSS, and given the ubiquity of browsers, a web app made sense for being both desktop and mobile. Due to being client side focused, I also figured I could easily host it on github pages to get something live quickly.
I've used the normal JS frameworks, used game frameworks that compiled to JS (most the games on my main page are built in KorGE), used Kotlin Multiplatform to run Quest Command as Js in the browser, and even built sites (like this one and my main page) with no JS at all. Lately I've flirted a bit with KotlinJS, first as part of the Quest Command multiplatform effort, and then in a spike at work. Impressed by its JS interop, general ecosystem and simply by how unexpectedly often things "just worked", I decided to give it a go for a larger project like this.
A Mobile Legacy
Writing a UI focused single page web app in Kotlin has been surprisingly exciting, and in the first 20 days I slammed down over 130 commits. KotlinJs is, while still feeling cutting edge, feels surprisingly well supported. Getting to put my business logic in Kotlin is great, but being able to define typing for external JS, including npm packages was neat, a little tricky and very satisfying. With just a little mapping code I was able to pull in a library for reading a zip file and later to use with local storage (more on that later) and then be back into a 'statically typed' world, even in using these vanilla JS libraries.
The app loads a default character in order to give users a 'sample', but the magic comes from compiling save files, exported characters, and a 'strings file' from the game into a zip and locally uploading it. The app parses the zip, reads all the characters out, identifies their pictures, and interpolates the text of their history events, doing string interpolation to turn templated base strings into lines that are customized by the character's name, gender, hometown, etc.
One of the things that I love seeing when I interview devs is a list of hobby projects. I've found devs with hobby projects are less likely to try to reinvent the wheel at work, because they've already done so at home. Using KotlinJS, but no framework, I've allowed myself to reinvent a number of wheels, just for the fun of doing it. After I had basic functionality down, I implemented routing from scratch. I hit a couple snags due to bad initial designs, but bad design on my part was really the only hiccup. I also created a fairly robust search that looks at character name, character aspects, personality, and class level. It took me an hour to implement; everything just worked. It's such a blast when each new feature just kind of flows out without hardly any resistance.
The largest challenge I had was with local storage. In order to not require the user to re-import the zip on every page refresh, I wrote everything (including base64 encoded images!) to json and stored it in the browser's localStorage. This worked great, was synchronous, loaded quickly, and had an easy interface. It also had a 5mb limit which I swiftly ran into with all the character images. In order to move forward, I removed my re-import constraint and continued building other things out. For a faster test loop, I added a zip file to the project resources (and git ignored it) and then told the website to grab that file and load it if it existed. This let me keep testing the full zip even without having local storage working.
I spent a good amount of time wrestling with if I should scrap the HTML approach and make an android app or something. Not being able to load files or have local storage was a bear. I couldn't auto-load a zip, and when I tried to allow users to enter a link to a zip (through google drive or something) and then store that link in local storage, I (rightfully) was blocked for making a CORS request. (Letting users load arbitrary links through json is a security nightmare). Googling around for local storage limits and workaround eventually lead me to indexedDB, which I discovered is an async JS DB built into modern browsers. And, to my surprise, it doesn't really have a size limit. Unfortunately, KotlinJS's std-lib doesn't yet have support for indexDB, so I couldn't natively call it.
Fortunately, KotlinJS has a neat feature where you can make calls to untyped, vanilla JS, and even still wrap those calls in typing. This is what I used to expose JS's Object.keys
function:
object JsonObject {
fun keys(obj: Any): List<String> {
val raw = js("Object.keys(obj)") as Array<*>
return raw.map { it as String }
}
}
Unfortunately again, the more complicated, async nature of indexedDB made it too tricky to use the raw JS bridge. Instead I tried a couple npm packages and was able to settle on the second one, localForage. Creating the mappings were really simple and I was able to essentially just persist my whole 'in memory store' to one key in the indexDB, and then read the whole thing back out on page load. It's simple, was easy to implement and reason about, and so far loads really quickly. I'm even able to persist search criteria, so reloading also reloads your most recent search (and the page also uses anchors to scroll to the card you were looking at, or show the details page, so you can even bookmark your characters).
@JsModule("localforage")
@JsNonModule
external object LocalForage {
fun setItem(key: String, value: Any): Promise<*>
fun getItem(key: String): Promise<Any?>
}
There are a number of features I'd like to add, but I'm really proud of what I've been able to build so far. I'm kind of shocked and elated at how easy KotlinJS has been to work with. Part of me is nervous that I'm starting to focus too much on one language, but on the other hand, it's been so convenient to work with that it's hard not to say my next front end project won't also use KotlinJS. It's also made me again wrestle with the usefulness of frameworks, but that thought would be served better by its own blog post.