This week involved a bit of navel gazing - what do I want this game to eventually be? What is fun and what is not?
Given that I started this as an homage to RotMG I wanted to start moving in that direction for the actual design and balance. The main concepts will (eventually) be large maps, loot, items that progressively get more and more useful based on enemy difficulty, etc. Spoiler warning - I didn’t get to the whole items bit, but it is good for future updates to consider.
Also some bugfixing and basic animation. Lets start there, shall we?
The animation was actually a lot easier to implement than I had thought. I’ve done this in the past in terms of client-side-only JS games, however receiving positional information from the server was going to be tricky (I thought).
It wasn’t really.
I always go back to the Broughlike tutorial for JS roguelikes. It involves adding an
offset attribute for the x/y coordinates and nicely degrading it as we animate in. I was able to do something similar.
MoveableEntity gets an
offsetY attribute on instantiation.
Drawing is as simple as tacking that onto my existing drawing function:
let _x = player.offsetX + (player.pos.c - startx) * tileSize;
let _y = player.offsetY + (player.pos.r - starty) * tileSize + tileSize;
player.offsetX -= Math.sign(player.offsetX) * (1/8);
player.offsetY -= Math.sign(player.offsetY) * (1/8);
This gets applied to all
MoveableEntity objects. The
Math.sign helps to lerp things a bit so the animation is smoother.
offsetY get set whenever the player presses a movement key. The nice thing here is that if the movement is invalid (wall, enemy, deep water, etc.) then the player’s position will revert back. The offset will give a mild stutter and look like the player is bumping into something.
(Right now enemies don’t make use of this field, but it is there for future updates).
One of the more egregious bugs I came across was that, in reworking enemy AI, I neglected to remove a targeted player if they become inactive.
That was a simple fix - right now I remove a target from an enemy if the player leaves the level. The fix was to also check if they were active.
# update the enemies
for e in self.enemies:
# (basic follower):
# 1 if no target, set target
# 2 if target, follow
# 3 if target out of range, forget
tgt = e.getTarget()
if tgt is not None and tgt in self.players: # we have a target
# same level test / meditating test
if self.players[tgt].pos['level'] != e.pos['level'] or not self.players[tgt].active:
# do the follow thing
Easy enough. Also highly abusable in a production environment, but we’ll save that for when it is time to balance this monstrosity.
As a fun aside, I fully anticipate that a large number of performance bugs will be cropping up, now that I’m going “to scale,” so to speak.
Let’s start with bumping the number of rows/columns to 2500 each. This image became necessary in the canvas as it takes longer to receive the whole map now:
I also started adding in a cellular automata for cave generation, but didn’t quite get it done in time for this post. There are a significant amount of tweaks needed as well, but at least it is something!
I based the implementation off of my favorite blog post for making CAs - they outline the algorithm quite nicely: A Bit Awake - Cellular Automata.
This was made while watching my kids playing outside so I gave 0 thought to tweaks and balance on all the parameters, but the skeleton is in place.
As you can see here, the maps are very stringy. I added a dev minimap to get a feel for what it looks like and clearly some optimizations need to be made.
That’s it for this week - next week I’m thinking game balancing, items, and bugfixes.