Michael Pidde => { Programmer(); }

Composition and Bindings

As sometimes happens, I'm taking another detour from the main Boreal Darkness development so I can mock out battle logic. I'm building a simple browser app so I can see a more visual representation of what happens when I change certain values and run through the steps of a battle. I'm sure that there are tons of battle logic simulators out there (probably for D&D nerds), but as per the usual, if I build something myself then I'm going to force myself to learn something along the way.

The first thing that I implemented which was new to me is in the form of object composition in JS. The idea of composition over inheritance is popular these days for good reason. Anyone who has been programming for some time has seen fragile class hierarchies, whether they created the mess on their own or had to wade through someone else's bothersome sandcastle. I wasn't sure of the best way to implement this concept in JS, so I stumbled upon this page which gave me a decent starting point. One aspect that I didn't like from that overview is the function naming for "what things do," that being things like "meower" and "friender." My complaint is pedantic, but bear with me. What something does is a verb. When you add the -er suffix to a verb, you get a noun. At that point you're no longer talking about what something does, you're talking about what it is, which puts you back in the same boat as what you were trying to get away from. If I saw "friender" in business code that I was maintaining, I'd be the proverbial guy with a shotgun. That aside, here's a portion of my own implementation:

const _takeDamage = (state) => ({
    takeDamage(base) {
        var damage = base / state.defense;
        state.health -= damage;
        state.log('\t -> ' + damage.toFixed(2) + ' damage.');
    }
});

const _isDead = (state) => ({
    isDead() {
        if(state.health.toFixed(2) <= 0) {
            return true;
        } else {
            return false;
        }
    }
});

const _log = (format = null) => ({
    log(msg) {
        var content = $('#log p');
        var container = $('#log');
        var out;
        if(format != null) {
           out = format.pre + msg + format.post;
        } else {
            out = msg;
        }
        content.innerHTML += out + '<br>';
        container.scrollTop = container.scrollHeight;
    }
});

function Player() {
    var state = {
        health: 100,
        defense: 1.2,
        attacks: {
            punch: 10.0,
            axe: 20.0,
            stick: 15.0,
        },
        container: $("#player"),

        attack: function(selected) {
            var multiplier = (rand(1, 9) / 10) + 1.0;
            var damage = this.attacks[selected] * multiplier;
            this.log('Player uses ' + selected.toUpperCase() + '(' + damage.toFixed(2) + ')');
            return damage;
        },
    }

    return Object.assign(
        state,
        _takeDamage(state),
        _isDead(state),
        _log(),
    );
};

My preference is to prefix composable functions with an underscore, hopefully to remind myself not to try calling them verbatim. I would get an error anyway, but at least scanning through the code it helps to see that syntactical reference.

Another thing that cropped up was the necessity for a good data binding technique. I only need one-way binding, and I was able to find a starting point here. Once again, I rewrote things a bit to accommodate how my own mind works through these concepts, and I find my implementation to be elegant for my own uses:

function oneWayBind(b) {
    if(b.format == null) {
        b.format = (val) => { return val; };
    }

    var state = {
        value: b.srcObj[b.srcProperty],
        getter: function() {
            return state.value;
        },
        setter: function(val) {
            state.value = val;
            b.bindTo[b.bindAttr] = b.format(val);
        },
    };

    Object.defineProperty(b.srcObj, b.srcProperty, {
        get: state.getter,
        set: state.setter,
    });

    // Prepopulate binding target
    b.bindTo[b.bindAttr] = b.format(state.value);
}

// Usage
oneWayBind({
    srcObj: arena.player,
    srcProperty: 'health',
    bindTo: arena.player.container.querySelector('.health span'),
    bindAttr: 'innerHTML',
    format: NumberFormats.scale2,
});