System: My game uses an "ECS + event queue" architecture where every tick, various events change the state of JSON-able data. Plain JS objects.

Problem: I want to be able to save the game-state as JSON, and there is one place where I run into an issue: multi-turn actions. For example, the player has an "intent" array that stores the next few stages:

// pseudocode:
let player = {
    health: 5,
    damage: 2,
    actor: true,
    intent: [ () => wait(), () => wait(), () => uniqueContextualStatefulAction()],
every tick
    for every actor with intent:
        execute and remove entity.intent[0]()

If all actions took a single "turn" or tick, there would be no problem, as intent would be cleared. But every tick, the function (callback?) of every actor is called (with some more complexity on the side). These callbacks are often unique and contextual, for example waiting for 5 turns, then crafting a custom item out of two others, then deleting the ingredients, all modified by status effects at the time of execution (rather than creation).

My question is as such. What is an "ECS-friendly", save-game-friendly, and sanity-friendly way to handle this system? At the extreme, storing every instance of every function and its arguments as strings, but I really don't... want to?

There's gotta be a better way. This is also deterring me from advancing the game systems, and I tend to go into over-engineering to avoid this problem.

Side-note: I am open to suggestions about changing some of the architecture altogether, since I can't imagine how to do this in other architectures either. Thank you!

An example of actual in-game code:

    function createWait(ticks) {
        return {
            effect: () => {},
            duration: ticks,
            pause: timing.msPerTick,

        durations: [
            { baseName: "1 tick", dur: 1 },
            { baseName: "3 ticks", dur: 3 },
            { baseName: "6 ticks", dur: 6 },
            { baseName: "12 ticks", dur: 12 },
            { baseName: "60 ticks", dur: 60 },
        intents: function() {
            let intents = [];
            for (let duration of this.durations) {
                function effect() {
                    newLine(`You wait ${duration.baseName}`)
                    representation: [game.word("wait"), game.word(duration.baseName)],
                    sequence: [{ effect }, createWait(duration.dur)]
            return intents;

1 Answer 1


If you need to serialize the actions, you should probably represent them as data (i.e. plain JS objects, not necessarily strings) instead of functions. Since you have different kinds of actions with different "arguments", the discriminated union pattern will help. This essentially means every action has a kind property, and you switch on action.kind in functions that deal with an action (e.g. to display it or to enact it).

  • \$\begingroup\$ The problem is, the actions end up being unique and contextual. By the time a character does a 5-tick heavy attack, they may get a buff that improves it. And with the crafting, items are unique based on their ingredients at the time. Callbacks help with that as they execute later, but I'm not sure how to do it as data. \$\endgroup\$ Oct 19, 2021 at 19:14
  • \$\begingroup\$ You can still have arbitrary code associated with each action that runs when the action is performed. In your tick function you'll just replace action() with enact(action), and write a function enact that switches on action.kind and delegates to the relevant implementation. By "unique and contextual" do you mean your existing action functions are closures with references to other objects which might change before the action is enacted? If so, you'll need a way to serialize those references too (e.g. using ids) to store on the action. \$\endgroup\$
    – Karl
    Oct 19, 2021 at 19:24

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .