This week was all about refinement and starting to prep things for procedural generation. Also, this post is probably going to be fairly terse as there were a lot of little updates and I have some other deadlines not related to this.

Here we go!


First of all we have a bit of a user interface refinement. First we have some relevant stats being reported in the game window sidebar (console.log was getting annoying), some animated CSS background trickery to spice things up, and a new font, Chivo Mono, c/o Google.

For the animated background I just gave my main window a pair of drop shadows and animated their blurriness and placement. Nothing too crazy, but hits that vaporwave aesthetic that is oh so pleasing.

Screenshot

Here is the relevant code if you’re interested (we’ll talk about that little checkbox in a bit):

#game-canvas {
    background: #222;
    margin: 0 auto;
    display: block;
    width: 900px;
    /* 640 + 260 */
    height: 480px;
    position: absolute;
    left: 50%;
    top: 50%;

    margin-top: -240px;
    margin-left: -455px;
    /* 900/2 + 10/2 */

    border: 5px solid #eee;
    border-radius: 20px;
    padding: 10px;

    animation: moveShadow ease-in-out 15s infinite;
}

@keyframes moveShadow {
    0% {
        box-shadow: -15px -15px 45px 0 rgba(220, 0, 220, 0.25), 15px 15px 45px 0 rgba(0, 229, 255, 0.25);
    }

    50% {
        box-shadow: 15px -15px 145px 0 rgba(220, 0, 220, 0.25), -15px 15px 145px 0 rgba(0, 229, 255, 0.25);
    }

    100% {
        box-shadow: -15px -15px 45px 0 rgba(220, 0, 220, 0.25), 15px 15px 45px 0 rgba(0, 229, 255, 0.25);
    }
}

This is also a good reminder (to myself) that I need to download the font locally, as I was doing some testing in a place without WiFi and defaulting to monospace wasn’t nearly as nice to look at (for me).

We also have a series of dots in the background that are fudged via some linear-gradient calls and are the result of me googling “animated CSS background.” The only thing I changed here is the background color and the original was here: https://codepen.io/edmundojr/pen/xOYJGw.


Next up we have … Z-levels! I mean dungeon depth. Everything so far has been mainly proof of concept, adding the ability for the dungeon to have floors starts to make it a bit more real.

If you’ve never done this before, it is pretty straightforward. The game map gets a new dimension, that’s about it. This also means that all references to the map now need to check that dimension as well.

Instead of gameMap[r][c] we have gameMap[z][r][c].

And the pos attribute for all Entity objects get a new level attribute so we know on which floor they live.

Upon adding this I spent some time quashing bugs, notably one where you could kill enemies on other floors because I forgot to update my attack mechanism (so, if you were on floor 1 and the enemy was on floor 3, as long as you were checking the row/column you could still hit them…).

Another thing I added was stairs, though right now the player’s placement is a bit random upon entering and leaving them - work for future me.


After this I wanted to add a bit more non-tutorial bits to it, so we have some sounds! I took a note out of the Broughlike tutorial for managing them and found a nice royalty free sound pack by the group that did Charlie the Unicorn.

Now we have ambient sounds, sound effects upon all sorts of things, and whatnot. Right now they’re mainly placeholders and absolutely zero sound normalization has been done, but I like it!

The fun bit here is that the pack has a large number of “similar” sounds, so I included them all and provided a bit of randomness for when an event pops. For instance, there is a group of bat hit 1 through bat hit 15 sounds that are played if the player hits a monster.

For this I added a new global lookup table for each of my sound events with the event name and how many sounds are available.

// pick a random index for each sound on [1,len]
let soundMaps = {
    monHit: 2,
    playerHit: 15,
    pickup: 4,
    stairs: 7,
    spawn: 1,
};

So if a player hits a monster, I just grab a random number on [1,14] and tack that onto my sound player (snd is the event name and sounds hold the loaded audio files and are keyed on the name of the event):

playSound('playerHit');

// abstracted a bit
playSound = (snd) => {
    let idx = getRandomInt(1, soundMaps[snd] + 1);
    let key = `${snd}${idx}`;
    sounds[key].currentTime = 0;
    sounds[key].play();
}

I also bulk-converted all the wav files to ogg in the thought that it would be better for file size. I did 0 checks to make sure this was the case.


Two more things, camera and drops! First lets talk drops. I updated my Entity system a bit to include the type(s) of items that can be dropped as well as how many. Right now all we have are apples so that’s all that can be dropped. However this is setup in a nice little global loot table that I can balance later.

This table lists out all the relevant information for each thing in the game. A better way would be to sub-class out every single entity (as the Broughlike/TCOD tutorials do), but I wanted everything in one concise place.

Note: this is terrible for human error - everything is keyed on something typed out multiple times. Just keep piling on the technical debt to get to that minimum viable product.

LOOKUP_STATS = {
    'maxHP': {
        'player': 10,
        'gobbo': 2,
        'snek': 2,
        'rat': 1,
    },
    'sprite': {
        # moveables
        'player': '@',
        'gobbo': 'g',
        'snek': 's',
        'rat': 'r',

        # static
        'apple': 'a',
    },
    # name: list[(tuple(type, max, chance))] - random() > chance
    'drops': {
        'rat': [('apple', 1, 0.25)],
        'gobbo': [('apple', 3, 0.35)],
        'snek': [('apple', 5, 0.5)],
    }
}

I probably should also create better enemies at some point because writing these out reminds me that I have the sense of humor of a toddler.

That’s not terribly exciting though and is really just object/state management. What is fun is scrolling cameras!

Scrolling camera

This is always the bane of my existence because I always do this kind of thing by hand (note: Unity/Godot make this ridiculously easy). However, I’ve done it enough times now that I can cheat by looking at old code and just transplant it.

The basis is here: https://www.roguebasin.com/index.php/Scrolling_map

Essentially, we create a viewport around the player and only draw things within that viewport. Note - this is lovely for performance optimization as you only need to manage visuals for things in view. You do still need to update AI and whatnot for things off screen, but at least we don’t have to draw it all.

The scrolling camera and larger map are what will be the requirement for my procedural generation as I like large worlds, however keeping it constrained for this use case will be a fun challenge.


Last but not least, I started working on the README file so that I could remember how to do things in the future when I invariably run out of time. The semester start is a month away and that means that it is nigh time for class prep to begin. How to add enemies and items, etc.


Next week I’m planning on adding proper procedural generation, so hoping to either make use of the tcod library for that or opensimplex. Perhaps some drunken walking for caves, we shall see.

Also a slew of bugs/issues to fix, including player placement from stairs.


Last post

Next post