TIC-80 in JavaScript

I set out to participate in this quarter’s #FC_JAM a few weeks ago. I scoped out the various fantasy consoles available, trying to find something lightweight and fun to use. Most fantasy consoles are written in C and accept Lua scripts, but I found that TIC-80 released JavaScript support and that instantly convinced me. Since I’ve been studying so much JavaScript, getting to stick with that language was the appealing choice. The console itself is all-inclusive, with a terminal, sound, sprite, and map editors, and with artificial restrictions of 240x136 pixels display, 16 color palette, 256 8x8 color sprites, and 4 channel sound.

I fired it up, made a new file, and I was in business.

Starting Out

Starting out, I played with text effects and rendering to see what kind of interface I could make. I quickly found that the default fonts are very large, and it would look too cluttered to use them. Instead, I used their various properties to animate them and create strong title text.

I started out kind of cheeky, but it was pretty easy to get a fun aesthetic going on the screen. The console is intuitive, and the built-in functions abstract away the annoying parts of trying to emulate this look.

The library worked as expected in JavaScript, but having just learned a lot of ES6, I wish I could’ve used it here. I thought of transpiling my code with Babel but it wasn’t worth it. Due to the memory constraints, brevity is key and there isn’t room or need to use those new features. That said, the PRO version of TIC-80 allows up to 8 memory banks so you could really cook with fire if you wanted to.

While I wanted to dedicate myself fully to making a game for the jam, I also wanted to take my time and get comfortable with the tool. Simultaneously, I was pretty busy at work with some development projects, so I was experiencing some burnout in the evenings. That said, I focused on trying to execute two things to frame a simple game:

  • a state management system
  • a platformer system
State Management

I was unsure what to call my starting project, but after drawing some sigil-like glyphs in the sprite editor, I created a menu screen:

I was able to draw any size centered text with a helper function. It allows for y-offset so you can move the text up or down while keeping it centered at the correct size. I overlaid two similar colors on the palette to give it depth.

var printCenter = function(string, color, size, yOffset, xOffset) {
    print(string,Math.floor((240-(print(string,0,-50, color, undefined, size))-(xOffset || 0))/2),Math.floor((136-6)/2)-(yOffset || 0), color, undefined, size);
}

State management was simple to configure. Game states were represented as objects with functions:

var states = null;var Menu = {
    init : function() {...},
    update : function() {...},
    render : function() {...}
    }
};

function TIC() {
    if (state != null) {
        state.update();
        state.render();
    }
}

The TIC() function is the game loop of the console’s engine. It fires 60 times per second to produce the framerate of your game. Since we’re using the same interface for each game state, we can partition code for each state’s logic in update() and handle displaying the results with render(). I created a Menu state to start.

I wrote a init() function in my game as well, to set up the game state prior to running the game loop. I used this function to start the time and set the first game state.

function init() {
    t = 0;
    time = 0;
    score = 0;
    Menu.init();
}

init();
Platformer

The working title for my game was Priestess, because I drew some cultish sprites and a red-robed woman who became my main testing avatar for the game. The main game state where the platformer would happen was called Town because the initial idea was to draw out a town. However, I quickly realized I didn’t have the sprite freedom to achieve anything dramatic in this short span of time.

Starting with the init() function, I set the camera’s coordinates to be the player’s location minus half the height and width of the screen. I also set this object the same way in the update() function every time it fires. Later in the render() function this will allow me to draw the tiled map around my character, keeping the camera centered like a traditional platformer. I also set the player’s location to the starting place on the map. Using the CELLconstant, I code clearly in cell sizes, which are 8 pixels.

var Town = {
    init : function() {
        state = Town;
        camera = {
            x : player.x-120,
            y: player.y-68
        };
        player.x = 8*CELL;
        player.y = 9*CELL;
    }

The update() function would performed the keybindings to move and animate the sprite with helper functions, eventually. After that, this function applied gravity to the map to produce a basic platformer.

The green tiles are sprite drawn onto the map with the built-in map editor. The white cube represents the player. In the corner you can see some of my debugging numbers. The cardinal directions indicated which corners of the player were touching solid tiles. Gravity carries the player down too slowly in this iteration, it would later be improved. You can also see the start of a basic “Game Over” screen that fires when something happens (in this case, when the player walks off screen).

Checking if the corners of the player are clear is done with a helper function:

var openCell = function(x,y) {
    switch (mget(x/CELL, y/CELL)) {
        case 1: return false;
        case 2: return false;
        case 6: return false;
        default: return true;
    }
    if (mget(x/CELL, y/CELL) == 0)
     return true;
    else
     return false;
}

I used a switch to check the ID of the sprite against whether it’s solid or not, to determine if the cell is open or closed. This function is used to check the appropriate corners of the player when they press directional keys to move.

With the basics down, I started to play around with the render() function and the built-in functions of the library to manipulate the screen. One of my favorite experiments created a glitch effect that sequentially corrupted the sprites, maps, and sounds actively during play.

To draw keep the game centered around the player, and draw the map in relation to the player, I used a camera object with x and y properties to track the player. I set the coordinates of this object to be the player’s location minus half of the width and the height of the screen. This allowed me to draw the map around the player and keep the screen centered.

map(0,0,240, 136, -camera.x, -camera.y, 0);
spr(player.sprite,player.x-camera.x,player.y-camera.y-CELL,0,undefined,player.flip,undefined,player.spr_w,player.spr_h);
Embellishments

Once I got to this point, I started to get a lot of ideas about where to take this basic platformer framework. What I had so far was very sparse, so I added a background image and scaled it up to render behind the player. By translating it across the screen by relational coordinates of the player, I produced a low-bit parallax effect.

spr(136,-15-(player.x/6),-34-(player.y/8),0,5,undefined,undefined,8,8);

I ended up taking this one step further to add clouds in the background, drawn from sprites and moved at various speeds across the screen, to give more depth to the parallax. Given the low resolution, the result is surprisingly nice.

spr(104, 30-(player.x/8), 10-(player.y/100), 0, 2, undefined, undefined, 2, 2);
spr(106, 140+(player.x/4), 11-(player.y/100), 0, 4, undefined, undefined, 2, 2);
spr(108, 100+(player.x/5), 20-(player.y/100), 0, 3, undefined, undefined, 2, 2);
spr(110, 30-(player.x/2), 0-(player.y/100), 0, 6, undefined, undefined, 2, 2);

As you can see, I also added animations to the character that can can stack relatively to each other. The priestess has a natural chest breathing moving, and when moving from side to side she also executes a movement animation. The shoulders rise and fall in her animation in the same timing as standing still. I was able to achieve this with a helper function that abstracted animation and made it possible for any object on the map.

var doAnimation = function(object){
    var spr_default = object.spr_default;
    var spr = object.sprite;
    var frameCount = object.frame_count;
    var frame = Math.floor(Math.random() * frameCount);
    if ((spr + frame > spr_default + frameCount) && (t % 60 == 0)){
        return object.sprite = spr_default;
    }
    if (player.is_falling == true) {
        return object.sprite = 179;
    }
    else{
        object.sprite = (spr + frame);
        return frame = null;
    }
}
#FC_JAM

The jam itself was hosted by egodorichev and trelemar and the theme was “One Minute” which was admittedly tough. I had a hard time reconciling this theme with my initial ideas. This gave rise to the glitch effect shown above, which was set to fully corrupt the game cartridge after exactly 60 seconds of play. This was my initial take on the theme before I started experimenting with parallax and animation.

There was a bit amount of chatter on the Twitter #FC_JAM hashtag for this jam, and as of this writing, everyone is just waiting to vote for their favorites.

There were a lot of great entries, and you should check them out if you’re interested in learning more about fantasy consoles.

I followed the development of several of the games while working on my own project, and there were three that really stood out to me. @AndreiRudenko‘s Space Pet Rescue has an immediately eye-catching aesthetic and surprisingly simple controls. The concept is just like it sounds, and it’s fun! The gameplay works perfectly for the average amount of time someone is playing a fantasy console game. It even supports two players!

@Mykie‘s The Witching Minute takes a simple concept and executes it really well. You’re a ghost haunting a house, and you’re trying to scare a family out of the building within a certain turn limit. You have to predict where the family members will move based on your previous hauntings, and scare them all out the front door at the same time. The creator did a great job creating a large number of levels, I found myself playing this game longer than any of the other submissions, and it didn’t feel tedious. The artwork is crisp and simple, and the entire game works perfectly for the theme.

@OdwardFrenry‘s Ship Reck’ is the most visually striking of all the games. Immediately from the first teasers on Twitter, I could tell that this game was different than the others. Composed of a sequence of puzzles, you’re the navigator for a sea vessel and responsible for guiding the ship safely on its course. Armed with an astrolabe and a few other tools, you interact with abstract systems that provide a puzzle without making it a chore. The graphics are mostly pixel drawn and rendered with striking geometric lines. This helps the game escape the easy square trappings of a fantasy console and explore irregular borders and tiny, incongruous shapes. Subtle details on rope, gold, paper, and wood produce a visually pleasing game. I only had slight trouble navigating the various menus and experimenting with what kind of actions I could take. Admittedly, I felt like I bumped into a wall a few times before I truly grasped how the game was designed. But the period of not knowing was surprisingly fun and still engaging, I resisted closing the tab until I figured out how to succeed at the game.

Resources

There’s great documentation available for the TIC-80 API so please take the time to check it out if you’re interested in the console. There are tutorials available as well on the same wiki. Most of them are provided in Lua, but it’s very simple to convert most basic concepts between the two languages, so if you wish to work in TIC-80 in JavaScript, it’s certainly approachable!