(blurred background image)

Espressomancer 🧙‍♂️☕️👍

This game was made for the 2023 Coffee Jam, which had an extra theme of “spooky”.

While the development version is still playable below, the finished version of the game is published on itch.io and is playable there. If you wanna leave a rating and a comment I’d be much obliged.

The Game

You are the proprietor of a coffee cart at the entrance to a magical labyrinth, catering to a procession of fantastical creatures.

  • Provide customers with their orders
  • Use your machines to transform items into other items
  • Call up your suppliers to get new items
  • Customers want predictable things
    • wizards always want a latte, elves want a cappuccino, etc
  • There’s no money! It’s an anarchist utopia
  • Get as many 👍s as you can within four minutes
  • If you have too many items (>8) a mouse will start stealing them
Executing espressomancer.mjs

Or there’s a fullscreen version that is a little nicer on mobile.

The game uses SFX from the FilmCow Royalty Free Sound Effects Library and Sidearm Ultimate Sound FX Bundle.

Design postmortem

My takeaway from this one is that cutting features is fantastic, and playtesting is extremely useful. Originally Espressomancer was planned to be a far more complicated beast, involving such wild ideas as:

  • Managing a layout of tables and machines in your cafe
  • A tinker vendor who’d sell you new & upgraded machines
  • Coffees granting your customers different bonuses as they went off to adventure
  • Customers returning from adventure with special beans, milks, flavourings & treats
  • Customers getting annoyed and leaving
    • with different customers having different patience levels
    • with a baker vendor who gave you 🥐s and 🍰s to manage frustration
Early gameplay loop diagram
Early gameplay loop diagram

But I had the sense to get an early prototype in front of some friends and loved ones and see that adding all that stuff wouldn’t really make it any better, and I focused on polishing the core loop instead. A few things had nice little knock-on effects - originally chocolate didn’t need to be ground, you just mixed it directly with the espresso, but my partner expected to grind it and that made sense! And then with chocolate powder added, the fairy customer sprang up, which also led to the ghost.

The ghost is my favourite part of the game. Without him the milk is a little less interesting - there’s never a reason to hold off on putting milk in the stove otherwise. But also it’s just really funny to me that a ghost wants cold milk. It just makes intuitive sense, but I can’t give you a reason that it works. They’re both white and cold I guess? Anyway. #1 feature: ghost.

I did intend the play experience of this game to be a lot more chill, but as the game jam deadline approached and I didn’t have a particularly good idea of how to make it wrap up I just slapped a “get points within a time limit” goal on it, which works (and I think suits this game just fine), but is definitely a shortcut to a more stressful and frantic experience.

So my goal for the next project is to go much more turn-based, which I think will be a challenge - how to make a turn-based game interesting without also making it complicated? I don’t really know. Let’s see if I can figure that out!

Technical postmortem

Preact + goober + esm.sh

In my day job I use a lot of React and styled-components, and Preact & Goober aim to be (almost) drop-in replacements for those libraries respectively, so I took this opportunity to kick the tyres on them. They seem to work just like their counterparts; certainly I got away with pretending that they’re exactly equivalent and didn’t run into any troubles. So that’s… nice? It’s kind of disappointing because their close equivalence meant there was no feeling of exploring new tech, just the knowledge that the download size would be slightly smaller for the user.

I’ve also used esm.sh to avoid having to set up a build system, and that’s worked very well. It’s really nice to avoid having to manage dependencies.

Basically esm.sh is a CDN that you can import your JS modules from directly:

// reading from a locally installed module that needs bundling
import { Component } from 'geotic'; 

// works identally, no build/yarn/npm necessary (but doesn't work offline)
import { Component } from 'https://esm.sh/geotic';

The downside is that not going through a bundler means that I can’t use JSX syntax. Instead, I used a module called htm that lets you use Javascript’s template strings functionality to achieve something similar.

// ## with a JSX build step, for comparison...

const Component = ({ prop, otherProp }) => (
  <Container>
    <InnerComponent value="3" innerProp={otherProp} />
  </Container>
);

// ## ... versus with htm...

// some boilerplate to set up
import { h } from 'https://esm.sh/preact@10.18.0';
import htm from 'https://esm.sh/htm@3.1.1';
const html = htm.bind(h); // link htm to preact

const Component = ({ prop, otherProp }) => html`
  <${Container}>
    <${InnerComponent} value="3" innerProp=${otherProp} />
  </${Container}>
`;

This feels both better and worse, honestly. Going through a template string rather than a build tool transformation step feels more “pure javascript” and is more coherent with the syntax of the rest of the language, and I like that a lot.

Conversely, the preponderance of ${}s inside each element tree feels quite ugly and annoying. The closing tags are particularly offensive to me for some reason.

So, I’m not sure I’ll stick with this particular approach. It’s worth noting that the build system that I’m using (the one that comes packaged with Hugo, the blogging software I’m using) actually does include a JS bundling step anyway, so theoretically getting proper JSX support just requires looking up how to do that.

Edit (2023-10-04): Turns out all that was needed was to name the javascript files .jsx. This assumed I was using React but switching to preact just took an additional "JSXFactory" "h" parameter in the hugo js.Build step.

Animation

The app uses two separate animation systems.

The visual animations (machines shaking, customers entering and bouncing thank-you, items bobbing up and down, etc) are all done via CSS animations. I’ve not done much with CSS animations before, even though they’ve been around for years and years, and so I’m pleased to report that it’s a great system and I look forward to messing around with it more in future projects.

// create four distinct "slide in" animations so that
// an element gets assigned a different animation when
// its index changes, triggering a replay
const shoves = [0, 1, 2, 3].map((i) => keyframes`
    from {
      transform: translate(${5 + i*0.5}px, 0px);
    }

    to {}
`);

const thanks = keyframes`
    from, to {
    }
    
    50% {
      transform: translate(0px, -5px);
    }
`;

const CustomerAnimation = styled('div')`
  ${({ entity, index }) => entity.satisfied 
    ? `animation: ${thanks} 0.3s ease-out;`
    : `animation: ${shoves[index]} 0.3s ease-out;`
  }
`;

The react tree is also re-rendered once every frame. That is, all the main elements get their props refreshed about 30 times a second, via the native requestAnimationFrame method. This is the dumbest and least efficient approach to doing it, but the UI is simple enough that I think it’s fine? I’m used to building things with complex enough trees that massive re-renders matter, so I was expecting to be punished for my cavalier approach, but I am pretty sure I got away with it. I will probably use this approach in future projects as well but I’m counting down the days til it bites me in the ass.

Geotic & ECS

This is my third project using Geotic (and preact/esm/etc, with the others being Do Crimes Don’t Get Caught and the train station simulator) and I’m becoming extremely fond of it.

There were two gripes I had with Geotic; one was how you need to remember to register all of the components with the engine before initialising any of your systems, and the other was how defining prefabs is really verbose. Luckily for this project I remembered I’m a programmer so I just made a tool that managed component registration automatically & another thing that allowed defining prefabs more concisely and now I’m just on cloud nine with Geotic.

For my next project with I want to try to keep game logic off of components and inside systems. Geotic allows you to go either way but I think with Espressomancer the modules with inert components & 100% of the logic inside the system turned out to be more manageable, so I’m going to lean into that next time.

Audio challenges

The amount of hacking required to get sound working was great.

Challenge one: tap-to-start. In an attempt to prevent adware and other malices from bombarding the user with unwanted audio, browsers restrict the initialisation of the audio engine so that it can only happen in response to a user interaction. This isn’t too hard to work around, you just need to update your click/tap event handlers to start the audio if it hasn’t already, and make sure your sound players don’t break anything if that hasn’t happened yet.

Challenge two: formats. mp3 is a proprietary format, but Safari doesn’t support ogg vorbis. Which one to choose! (the answer is probably to suck it up and choose mp3). This took a few goes because I don’t have an Apple machine handy so I relied on messaging friends “try the latest build, any sound?” and reuploading things til it worked. It didn’t help that various posts I read about this informed me that ogg was still the safest choice for interoperability (it totally isn’t) and that vorbis was actually ok as long as you contained it in webm instead of Ogg (webm didn’t help).

Even after caving and switching everything to mp3, it still didn’t work on my friend’s iPhone. I caved and switched to using Howl for audio rather than rolling my own, which worked….

…briefly. Enter challenge three: CORS. Things were working fine when it was all hosted on mcccclean.com, but when I uploaded the build to itch it all fell over. Itch hosts things on AWS, with a security policy set up such that Howl couldn’t play the audio files I’d loaded.

CORS, Cross-Origin Resource Sharing, is a security policy that exists to prevent app developers from scraping sensitive data from external websites. It’s really common to load an image or video from an external host, but if it’s not done carefully you can expose unwanted data. For a slightly made-up example: imagine my game claimed that it needed facebook.com/my_profile.jpg as one of its graphics. Your browser asks for that image from Facebook, and Facebook, recognising you’d logged in, sent your profile image to my game. Now suddenly my little game, without you doing anything, knows a) whether you’ve logged in to Facebook, and b) what you look like.

So browsers implement this policy where your code can display externally-loaded media to the user, but it isn’t allowed to actually to inspect the contents of it. Makes a lot of sense, until we find out that the system that Howler uses to play audio is also capable of analysing it, and is therefore forbidden from doing either. The way to fix a CORS issue is to configure your server to explicitly tell the browser, “I’m fine with this media being used externally” - but Itch doesn’t grant random indie devs access to its AWS server settings. I was faced with a choice between “no audio on iPhone” or “no audio on Itch”. Neither were particularly desirable.

My final workaround was to just fake the whole system out, tricking it into thinking that Espressomancer’s sound effects aren’t actually audio at all. The sounds are all encoded as text (via base64) and embedded directly in the source code of the game. The server happily loads the whole lot thinking it’s perfectly innocent game code, and once that’s all arrived safely in memory the game pulls a little switcheroo and decodes a big chunk of it as a group of mp3 files, which can then be loaded into Howler and played back on any device. Whew.

This has the downside of making the game take a bit longer to load, as it can’t start the program until all of the sfx are finished downloading, but the whole lot still comes in at like 500kb. Not a serviceable solution for a bigger game, but that’s why I like working small.

This system was a pain to figure out but I felt really smart getting there, which is what matters. I’ll be using it again as well, although I definitely want to figure out a way to break up the payload so that the game code can start running without having to load all of the media; I guess this will be especially important if I want to include music.

Wrap up

This was a really enjoyable project, and a great way to spend a few evenings and a long weekend. It’s been received quite well by those who played it which is about as much of a goal one can expect to kick from this kind of project. I learned a lot, figured out a bunch of cool tech, and it’s got me actively excited to make something else soon.