
You can read the longer, more personal version of this post on my blog, and play Pestis Apotheca on itch.io.
PESTIS APOTHECA
2020 has surely been a year full of surprises for everyone (except virologists who had been warning people about pandemic readiness for decades, or pessimists, who knew things would likely take way longer to "get back to normal").
Back in December, I was out of ideas for #procjam, but I had some holidays that coincided with it, and if there’s one thing I needed was something to fill up my time during the days off (before you look at me like that, it was a tough, tough year and I needed to do something fun).
"I don't even know you, why are you being mean to me?"
Turns out that living in a mild post-truth dystopia is all the inspiration you need, especially spending 98% of your time inside a London flat (where the only thing as high as rent price is the chance of something malfunctioning). My partner and I, being a pair of introverts who got relatively comfortable with the lockdown hermit life, had our first glance at social contact in months with a plumber fixing our broken heating. Oh yeah, the plumber seemed to have a mild cold. Also, it was on the same week in which we got a new oven delivered to replace our broken one, by a nice gentleman with a cough.
A lot of people would immediately get angry. However, one must r/AmItheAsshole with oneself every now and then: I’m comfortably working from home and have tons of support from my company if I need time out. Do these guys have that option?
But it’s kind of comedic really: you might spend months sanitizing your groceries and quarantining your mail, and then one random encounter might get you sick. So how do you balance out your pyramid of needs between “having warm showers”, “being able to cook” and “avoiding getting sick”? How does the social element fit into all of this?
That scenario just clicked in my head: it would be my #procjam game. You’re fighting an unknown plague, trying not to get infected yourself. Originally, the game was going to be way more of a “social scenario” simulation: you’re an apothecary who has to deal with the customers and equipment breaking. Can you afford to have a a customer giving you a bad review because you sent them out for not wearing a mask? If your equipment breaks and the person doing the repairs has a symptom, do you send them away and risk not being able to craft medicine? That guy who just showed up with the latest rumors about how to treat the plague… can you really trust that information? But most of all, how do you deal with being sick with no one but yourself to rely on?
It’s funny how none of those mechanics ended up in the final game, but they’re essentially the root of it all. Speaking of roots, the name, Pestis Apotheca isn’t supposed to mean “plague pharmacy” or something like that; it’s supposed to be “plague repository“: there was going to be an underlying mechanic in which people would get infected by visiting your shop at the same time as someone else who had the plague.
(Yes, this is me insidiously inoculating the original idea into your mind without actually having to implement it. Take that, @PeterMolydeux.)
THOUGHT HERDING
In 2019, one of the best things about Vortex was that I was essentially just polishing an incomplete prototype, with lots of UI eye candy provided by thia9uera. Since that worked out well and this game would need quite a bit of UI, I asked if he was interested in another collab, and he was. Huzzah!
But as you can tell by the paragraphs above, my process on game jams tends to be very open ended: there’s a general idea, then I just start trying to throw parts of it to the wall and seeing what has enough time to stick before starting with the next coat of Jackson Pollocking the code. To attempt shaping things a bit more so I wouldn’t make Thiago do throwaway work, the first thing I did was a simple conversation system, and implementing a draft of the game’s tutorial with it. This was really good because it gave us a general design we would aim towards and showed some edge cases on the conversation UI. Mentioning this on twitter, I got this reply:
I’m not smart enough to have thought this through, so it was not intentional, but “tutorial driven game design” is not only a great assessment of what I did, but also highlights what could be an interesting workflow for future projects! Thanks, @kchplr!
We also used Notion which seemed to work well for getting everything written down in a single place. I've been using it ever since, even in solo projects!
LET’S TALK THIS THROUGH
The conversation system was pretty simple: dialogues were defined in ConversationData ScriptableObjects, which had an opening sentence and a list of options, containing the NPC’s reply or a link to another conversation. I added a flag to only show certain prompts after all others were explored to control timing (i.e.: force you to read my crappy jokes).
The only reason why a possibly cyclic graph didn’t get out of hand is because the depth of conversation options was really small. If you’re doing a full blown conversation-based game, it might be worth it using some FSM solution (like we did in Blood Runs Cold) |
That’s easy for static conversations, but what about the patients? The game is also partially inspired by the fact that whenever I go to the doctor, I tend to list all the information I can. If you’ve ever had to talk to me in person, you know that I might cram a lot of information in the same long-winded sentence, including useless information; i.e.: doctors probably hate me. Sorry docs!
But I imagine it might be a bit like listening to a bug report: you want as much information as you can get, and you kind of automatically filter things out that you know are unrelated. This was easily represented by the mechanic of clicking certain words to highlight symptoms: unless you “actively listen” to the patient, you won’t uncover what they’re feeling. Fortunately, TextMeshPro has a handy hyperlink tag that I could use to trigger those clicks.
To generate a “natural” sounding conversation, I used simple grammars. If you’re not familiar with the concept, it’s essentially defining the overall “format” of a piece of text via tags, then replacing the tags with words or chunks of text. Kate Compton and Emily Short have lots of great material on this approach.
Ironically, in all this time doing procgen this was actually the first time I wrote a grammar system – not just because I was never concerned with text, but also because it’s a considerable challenge to write the text in a way where everything fits and still feels varied. I probably should have gone for GalaxyKate’s Tracery instead of rolling out my own, but it was one of those “too busy chopping wood to sharpen the axe” moments.
Grammars were defined by tags ([starter] [primary] [finisher]) and tags were replaced by elements ([starter]=Good Day!;Hello!;Well met!) |
The system is just recursively going through all words between brackets and replacing them with content. Here’s the gist of it (this was all written in game jam mode and never revised, so proceed with caution if you want to use it).
For the patients, a ConversationData file with default parameters was instanced, then the contents of the relevant answers was replaced with something out of the grammar. The only tag left after the grammar processing was ‘[symptom]‘, which was then replaced by the patient’s actual symptoms based on convoluted rules that I wrote with a melted brain and I’m glad I don’t have to touch anymore.
The last detail was a hardcoded processing of “command” tags: if a tag like “[!EXIT_SHOP]” was present in the response text, I’d fire a Signal and the relevant code would take it from there.
ASTROLOGY
If you read the gist above, you probably noticed the System.Random being passed around. That’s because each NPC has its own random number generator, as does the world. A good side effect of that is having deterministic results that allow easier debugging, but the main intent was sharing the world among players. This means that you could, in theory, help somebody cure poor Coilbrit Millard, the apprentice gunsmith, or share the symptoms of THE PLAGUE you found out via the comments in the itch.io page. Sure, this depends on having an active playerbase, but shhhh… only dreams now.
Another (existential dread inducing) side effect for NPCs is that the mere act of choosing which shelves they were walking towards next would impact their chance of survival later on – which given the world I wanted to build with Bestiarium, is hell of on point.
The World RNG is used to create ids, which are then used as seeds on the individual RNGs of NPCs, diseases and herbs. Some of those elements require storing entire data instances (e.g.: disease state at day 3 for some NPC), but others can simply be restored from their id (e.g.: the effects and side effects of a given herb id). And before you ask, yes, the World RNG seed is, in fact, 2020.
DISEASE ENGINEERING
All diseases are generated by selecting r unique entries from a pool of n possible symptoms, with r=3 for default diseases. The game has 12 symptoms, so the good old n! / r! (n – r)! tells us that there should be then 220 unique diseases in the game, plus THE PLAGUE which has r=4. Symptoms are defined by an int-based enum and an associated configuration ScriptableObject.
After selecting a set of symptoms, their enum values are combined into a seed, which is then used to start up the RNG that generates disease name and any additional data. It’s a bit convoluted (World RNG selects N numbers -> mix those numbers into a hash -> use this hash as the seed to a new RNG), but it guarantees the determinism and that equal sets of symptoms don’t generate different diseases. I had just imported a xxHash for something else, so it was quicker to just use that to generate the hash from the set of enums rather than… you know, thinking of adding numbers multiplied by powers of 10.
TO RID THE DISEASE
In real life, there’s 2 main ways medications tend to work: treating the symptoms or treating the causes. For design purposes, in the game, treating the symptoms always cures diseases. Treating a patient down to level 0 in 60% or more of their symptoms causes them to heal completely. The diseases have a maximum of 3 levels and if any symptom reaches level 4, the NPC buttons up the wooden coat, as we say in Brazil.
With every passing day, symptoms that received medication go down a level, and symptoms that haven’t go up a level. To treat diseases, the player must concoct potions using different medicinal herbs.
Those herbs are straight out of Herbarium – as in, I copy-pasted the whole source folder then fixed the compile errors, so it’s using the good old flower generator as well.
Each flower treats 2 symptoms and causes 1 side effect which can happen with a given percentage of chance. This means that it’s possible that you can cure regular diseases with a single plant, but you can never cure THE PLAGUE (as it requires at least 3 symptoms being treated).
To increase the amount of sy