It’s been a very busy week. For the demo I only have 1 level and the vocab data is stored in a single hard-coded json file. I want to separate the data from the code before I start adding any more levels. I’ve looked into a few solutions for this and I’ve decided to go with Google Firebase.

My head has been spinning trying to figure out Firebase. Every time I learn one thing I realize that there are two other things I need to learn. All week I’ve had dozens of browser tabs open to various guides, API references, and StackOverflow answers. But things are starting to settle down and I’m in a good place for the time being.

Here’s a rundown:

User Auth

Users can now create accounts and login/logout from the Dead Language website. Firebase does the hard work here, but I had to spend some time getting the setup right and figuring out how to integrate it into the site. Also the website needs to interact with the game beyond creating it now. I had to make sure when the login modal opens up the game stops processing input, and reclaims control when the modal closes. Also I want to restart the game when the login status changes so the player can get back to where they were.

document.getElementById('modal-toggle').addEventListener('change', function() {
  if (this.checked) {
    window.game.input.keyboard.enabled = false;
  } else {
    window.game.input.keyboard.enabled = true;
  }
});

Cloud Data Storage

I saved all the vocab data for the demo level into Firestore, the database part of Firebase. Now instead of reading the data from a json file the game reads it from the database. Getting this first bit of data in was very tedious, as the Firestore UI is very slow and painful to use. For now I’m just loading all the lessons stored in the database when the game loads. Since it’s just text data I don’t think this will be a problem loading everything, even when I add more levels. But we’ll see.

loadLessons(callback, context) {
  this.db.collection('lessons').get().then((collection) => {
    this.lessons = collection;
    this.lessonsLoaded = true;
    callback.call(context);
  });
}

Data Security & User Profile Sync

Firestore has a robust security model for controlling read/write access, which took even more reading and head scratching to figure out. I want to make sure that anybody can read the vocab data, but only myself (or other future admin users) can modify it. Thinking ahead, I knew I was also going to need to be able to store additional profile information for each user, such as completed levels, stats, saved options, etc. Firebase auth doesn’t let you add this extra info, so I had to spend a long time figuring out how to add profile information in Firestore and keep it synced up with the users in Firebase Auth. The solution I ultimately came up with was writing a Firebase Function that is fired whenever a new user is registered. This function creates a corresponding user entry in Firestore, where I can save whatever info I need. Getting this working required setting up and learning all about Firebase functions *whew*.

exports.addUserToDatabase = functions.auth.user().onCreate(user => {
  admin.firestore().collection('users').doc(user.uid).set({
    email: user.email,
    admin: false,
  });
});

Data Management & Data Sync

And finally, my tedious experience adding vocab data in the Firebase UI made me realize I was going to need a better way to import data if I want to add a bunch of levels. So I wrote a NodeJS script that reads data from a Google Spreadsheet using TabletopJS, formats it for storage, and writes it to the lessons collection in Firestore. Now I can use the much faster & friendlier interface in Google Spreadsheets to write/manage my game content and run a single CLI command to sync it all to Firestore. The code for this is hideous, but that’s okay. If it grows or gets more complicated I might have to reevaluate, but it works for now. You can check out the github repo if you want to see the whole thing.

// load all vocab in the sheet
const rawVocab = tabletop.sheets(sheetName).elements
// cleanup vocab (remove blank strings, split alternatives array)
const vocab = rawVocab.map(function(vocabEntry) {
  const alternatives = vocabEntry.alternatives.split(',').filter(e => e != null && e !== '');
  return {
    id: vocabEntry.id,
    language1: vocabEntry.language1,
    language2: vocabEntry.language2,
    partOfSpeech: vocabEntry.partOfSpeech,
    gender: vocabEntry.gender !== '' ? vocabEntry.gender : null,
    alternatives: alternatives.length > 0 ? alternatives : null
  }
});

// save lesson data to firestore
firebase.firestore().collection('lessons').doc(sheetName).set({
  name: metadata.name,
  vocab: vocab
}).then(() => {
  console.log(`Document ${sheetName} successfully written!`);
}).catch((error) => {
  console.log(error);
});

I’m happy that I’ve learned more about Firebase. It’s really powerful, and learning about how the different components can interact opens up a world of possibilities. That said, this was an exhausting week, and all of this infrastructure setup felt like work rather than a fun hobby. Bleh. I’m excited to get back to feature development!