This is gonna be a mess of a thing to write because while it's not objectively my worst gamedev fuck up, it is really funny.
Maybe you've played the game and didn't quite get why it didn't work - the demo available in the morgue is pretty solid, actually. But the problem with it was more........ structural, specifically, that I really warped JSON beyond recognition.
After I wrapped up Tiny Dungeon, but before I shipped it (there was about 6 months between finishing it and launching it - see: seasonal affective disorder) I really wanted to do something with Time Chunch again. It was more or less my baby. And it still is. But it used to be, too. However, I had learned things from my time with TIC-80 on Tiny Dungeon, and the most important thing was "game feel is good, actually", and that animating things could be fun! Colors and artwork add a lot to a game, surprisingly. Unfortunately, the current state of Time Chunch was unfeasible for that - the ASCII output sure didn't do it any favors, or at least, my unwillingness to really make the most out of that ASCII output, for sure, but beneath that, the abysmal framerate and optimization just didn't help, and learning how to optimize things was way out of my wheelhouse at the time.
The first attempt to fix this was to edit the code page 437 tileset that came with ROTLove, the system that I used for displaying ASCII text. This went fucking nowhere - tiles were still hard to animate, due to the framework, and eventually I just scrapped the whole thing, sealing it off and calling it "TIME CHUNCH, MARK 1". (postmortem) And then I started over, doing something new - using graphics.
I'm gonna skip over the "what went right / what went wrong" structure, since a lot of my positive feelings about the original Time Chunch carry over to this one - it's still a good concept, and the biggest difference between this implementation and the last implementation is mechanical - you now have two meters. One is red, with 5 pips. That's your health. One is blue, with 500 pips. That's your time bubble.
You could press X to turn the time bubble on and off, and when it was up, you took "more damage" but from a larger pool, and with it off, you only ever took 1 damage, but from a pool of 5, and you could never, ever get that red health back. Along with that, your time bubble decreased in health while active - every turn it'd drop by one, and you'd wind up running low between floors. This created an interesting dynamic of either having the bubble up, and being safe, or having the bubble down, and saving it. This was a really, really well-designed mechanic in a way that I really think I did a great job with. When Time Chunch mark 3 rolls around, I will definitely be bringing that one back. Because it owns.
So here's the big one.
So here's an extremely fucked up cascade of bad decisions.
I started loading the game's assets with the very nice LOVE2D library, Cargo. What Cargo did was load entire folders into a table, that could then be traversed like a normal table. Why would anyone ever use something like love.graphics.newImage('assets/sprites/player.png')
when you could reference game.assets.sprites.player
? So this made assets a table that had to be traversed rather than strings to reference. This was maybe not the most immediate mistake - it's probably a fine library when it's not being used by a dumbass who will break things.
Along with this, one of the features I wanted was the ability to have a real-time save - the game was turn-based, still, and it seemed easy to pull off - I already work with a really simple ECS-like structure, just don't have any components keep data that can't be serialized to JSON, and then when the player hits the quick save button, serialize it all and write it to "quick_save.json" or whatever the fuck. Ez, gg, no re. Duh.
Along with this, I had trouble in Oh! My Boy! (see the morgue for that one) with getting entities to work nicely - a lot of objects were extremely hardcoded and tough to work with, because of a lot of bad structural ideas, at least part of which is playing nice with Cargo, but also, in Time Chunch MK 1 and Oh! My Boy!, aside from names and classes, game objects don't have any structures inherent to them, they're all controlled primarily by components. (A fancy word for Lua tables with an update
function.) This was a rough structure because it really banally increased time for writing code - the difference between Game.world:getObject('player').location.x
and game:get('player').x
when it comes to coding is so extremely a lot - especially when you want to refer to things in a hierarchy. This was a pain, but not a game breaker until I wanted to start moving faster and spending extra time writing out location
and Game.world:getObject('player').behavior:getBehavior('PlayerBehavior')
made coding just... miserable.
So, instead of fixing this mistake, I doubled down on it! Unintentionally, of course. I was bad at predicting things.
So, entities were stored as JSON, with all the details that differentiate them from the default entity saved in a JSON file, and scenes were stored as JSON too - this made it easier to implement custom entities. (These days "scenes" are more abstract states that don't ever get saved - there's a single scene.lua
file that contains the processing order for the core systems and I load objects and such from Tiled / REXPaint / whatever map editor I'm using.)
The most important thing though, was that entity behaviors were saved as JSON.
You generally want to avoid this, because behaviors are extremely code-driven.
In my mind, this was with the intent to eventually go through and build a scene editor, something I could use to quickly build skeletal enemies, scenes, tile maps, etc. I was really gonna go all out with reinventing the wheel, since it might not be particularly efficient but I was not bound by limitations, and I am always eager to learn new things. If it means understanding simple machines better, fuck yeah I'll reinvent the wheel.
But in practice, I never made the scene editor, since making UI that works is still something I struggle with, even recently it's been tough.
So the thing about this is that it ripples out into development, and here's how that affected development. If you look through the source you might find some fun things. Most of all you'll find a lot of scripts for extremely fucking mundane things. The majority of scripts are only 3 lines long, processing tables into feasible arguments for functions that should have just been called directly.
Here's a few.
return function(params)
addTurns(params.amount)
end
return function(params)
if params.format then
for index, param in pairs(params.format) do
params.message = string.format(params.message, param)
end
end
print(params.message)
end
return function(params)
if params.entity then
game:remove(params.entity.parent)
end
end
This is an absurdly fucking stupid thing to have exist, no? Like, in a language with less compunctions about running, this would maybe make sense but this is Lua, you can write Lua out by hand and it'll probably run.
But why would this exist? What leads someone to do such a thing?
I'm gonna break this down. Here's the script create_item.lua
.
return function(params)
local id = i(params.id)
local pos = getLocation(params)
if id then
local item = game:add('item')
item.item = id
item.animation.current = id.sprite
item.behavior.scripts.hover[1].format[1] = id.name
item.behavior.scripts.interact[1].item = id.class
item.behavior.scripts.interact[3].format[1] = id.name
setGridPosition(item, pos.x, pos.y)
return item
else
error(string.format('Attempted to create an item and failed. ID: %s', id))
end
end
It's hardly more than 10 lines but it's so extremely a good choice to explain something that would otherwise be a pretty simple mechanic - start off by passing the params
table to the function via... something... and then:
we make sure the item exists, and isn't bullshit, and get a copy of that item from the main table,
local id = i(params.id)
make sure the world position isn't some horseshit either,
local pos = getLocation(params)
and then if it's valid,
if id then
we begin building the item object, the sprite that appears on the map, from the item
template.
local item = game:add('item')
Once we have our item object, we set the item ID to the item we wanted to spawn.
item.item = id
Since the items are all collected on one sprite image, we use the animation component to distinguish between items. Thus, setting the animation component to the proper animation for our new item will make that item appear properly in the overworld.
item.animation.current = id.sprite
All fairly normal thus far - these are all still ideas I use to this day, in some form.
But here's where things get fucked up and horrible. I compulsively add hooks to things - if I think an object will have X interaction, you can bet real money on the behavior component template having a X()
function on it, as well as preX()
and postX()
and noX()
. It's both a character flaw and a bonus part of being me. It helps make things just a little bit easier, now that I have it under control, but here, it's at its worst.
With this implementation, there are 19 unique calls for an object with a behavior to have. In order, you've got your standard init
, update
, draw
, remove
. You've got collision, and item use with collide
and use
/ use_item
. (No, I don't remember the difference.) You've got interactions with the cursor - hover
and interact
. You've got your turn
action, you've got health management hooks - damage
, time_out
, as well as combat hooks - shields_up
, shields_down
, attack_success
and attack_fail
as well as defend_success
and defend_fail
, and death
.
These were buried quite deep in the object so we've already got issue #1 - to access a behavior hook, you had to go into entity.behavior.scripts[hook]
, which is three layers deep. However, the second thing is even fucking worse.
These objects were saved as JSON, so there is nothing to call here. These are not hooks. Because scripts are referenced by their position in a table. So each key in a table is stored as a value in a list. So instead of being a normal person and referencing the script assets/scripts/inventory/add_item.lua
, you would instead have to reference ["inventory", "add_item"]
. That's only for shorthand though - you can't pass parameters though. This also sucks because you can't key JSON objects with lists, either. So each call is its own entry on a list.
Eventually, you get into recursive scripts - it's just abysmal.
So where were we in this breakdown?
item.behavior.scripts.hover[1].format[1] = id.name
item.behavior.scripts.interact[1].item = id.class
item.behavior.scripts.interact[3].format[1] = id.name
setGridPosition(item, pos.x, pos.y)
Each call to a script was written out as a JSON table, first. More time was spent tweaking JSON files to make sure that the game didn't shit itself and die than it was making the rest of the game - 3 months of work was spent largely on what is effectively transforming JSON into the most fucked up abomination I've literally ever seen.
So that's why I gave up on Time Chunch a second time.
Overall, this story has a happy ending - Slimeworks, the current framework I use for every game, including the quite good witch machine, earlier this month, was heavily informed by the extreme mistakes made building this.