Hacking squiffy further…

I was playing around with something today, which would have been a lot easier if I could modify one of Squiffy's built-in functions, setAttribute.
Now, I'd already written a script that would let me modify functions like squiffy.ui.processText or squiffy.story.go from within a game. But setAttribute is harder, because it's a local variable - it isn't in scope when user code is being executed. After a little poking around, I found a way to get it. This basically uses the fact that when a game is loaded, the code in the _transition attribute is passed to eval - and has access to any local variables that were in scope at the point where squiffy.story.load is defined. So I force the game to save and then load immediately on startup, so that I can use that eval to run my code.

Just posting in case anyone else finds it useful (or can tell me a simpler way that I've missed

(this javascript goes in the starting section; after it finishes it passes the player on to section "firstsection" which will contain the real start of the game)

    window.sq = squiffy;
    var transition = function () {
        console.log("Initialising...");
        var setfunc = setAttribute;
        var processtextoriginal = squiffy.ui.processText;
        squiffy.story.save = function () {
            squiffy.set('_output', squiffy.ui.output.html() || "  ");
            squiffy.set('_transition', squiffy.get('_transitioncode'));
        };
        setAttribute = function (expr) {
            var matches = /^(\w*\s*):=(.*)$/.exec(expr);
            if (matches) {
                console.log("Matches");
                console.log(matches);
                expr = matches[1] + '=' + squiffy.ui.processText(matches[2]);
            }
            console.log("Evaluating set: "+expr)
            setfunc(expr);
        };
        squiffy.ui.processText = function (text) {
            // insert modified text processor here
            return (processtextoriginal(text));
        };
    }.toString();
    squiffy.set('_transitioncode', transition);
    squiffy.set('_transition', transition);
    squiffy.set('_output', ' ');
    squiffy.story.load();
    squiffy.story.go("firstsection");

In this example, I'm tweaking it so that you can do things like:

@set someAttribute := This {anotherAttribute} will be substituted when the line is parsed

So I can change anotherAttribute and someAttribute will still contain the old value :)

I also tweaked the text processor so you can add arbitrary new commands to it (the same way I already did for Quest); but that's not really the point of this post.
I just thought it might be interesting, in case anyone else wants to modify some of Squiffy's "private" internal functions in your games.


Didnt get what it may be good for. Do you have any example?


Didnt get what it may be good for. Do you have any example?

This is basically "how to modify the built-in functions". Like if you want to change how Squiffy works in some way to better fit your game; making functions that run every time you visit a new page, or adding new capabilities to the text processor, for example.

The system I was building it for looks something like this:

        squiffy.ui.processText = function (text, data = {}) {
            if (!squiffy.ui.textProcessorFunctions) {return (processtextoriginal(text));}
            var args, building = '';
            var depth = 0;
            var output = '';
            var command = '';
            $.each(text.split(/(?=[:{}])/), (i, token) => {
                if (depth) {
                    if (token.match(/^\}/) && (depth == 1)) {
                        args.push(building);
                        building = '';
                        var cmdname = args[0];
                        if (squiffy.ui.textProcessorFunctions[cmdname]) {
                            args.shift();
                            output += squiffy.ui.textProcessorFunctions[cmdname].apply({command: command, data: data}, args) || '';
                        } else {
                            output += command + '}';
                        }
                        command = '';
                        output += token.substr(1);
                        depth = 0;
                    } else if (token.match(/^:/) && (depth == 1)) {
                        command += token;
                        args.push(building);
                        building = token.substr(1);
                    } else {
                        if (token.match(/^\{/)) {depth++;}
                        if (token.match(/^\}/)) {depth--;}
                        building += token;
                        command += token;
                    }
                } else if(token.match(/^\{/)) {
                    building = token.substr(1);
                    args = [];
                    depth = 1;
                    command = token;
                } else {
                    output += token;
                }
            });
            if (command) {
                output += command;
            }
            output = processtextoriginal(output);
            return (output == text) ? text : squiffy.ui.processtext(output, data);
        };

Which is still just a framework to modify more stuff. Like a random function…

    squiffy.ui.textProcessorFunctions = {
        random: function (...options) {
            return options ? squiffy.ui.processText(options[Math.floor(Math.random() * options.length)], this.data) : '';
        }
    };

So I can do something like:

You walk through the garden and notice a pretty {random:red:green:yellow} flower.

or

@set cointoss := {random:heads:tails}

(this is all off the top of my head, I didn't finish writing it yet so I haven't tested all the stuff in this post)

(and I know this has some real problems… like the fact that I can't access the data object, because the existing text processor doesn't expose it)


OK… putting the pieces together now…

This is something like what I originally envisioned. I know it's a huge chunk of JS to put in the first section of a game, but I think it makes the text processor a lot more flexible. So you don't need as much javascript later on.

    window.sq = squiffy;
    var transition = function () {
        console.log("Initialising...");
        var setfunc = setAttribute;
        var processtextoriginal = squiffy.ui.processText;
        squiffy.story.save = function () {
            squiffy.set('_output', squiffy.ui.output.html() || "  ");
            squiffy.set('_transition', squiffy.get('_transitioncode'));
        };
        setAttribute = function (expr) {
            var matches = /^(\w*\s*):=(.*)$/.exec(expr);
            if (matches) {
                console.log("Matches");
                console.log(matches);
                expr = matches[1] + '=' + squiffy.ui.processText(matches[2]);
            }
            console.log("Evaluating set: "+expr)
            setfunc(expr);
        };

        squiffy.ui.processText = function (text, data = {}) {
            if (!squiffy.ui.textProcessorFunctions) {return (processtextoriginal(text));}
            var args, building = '';
            var depth = 0;
            var output = '';
            var command = '';
            $.each(text.split(/(?=[:{}])/), (i, token) => {
                if (depth) {
                    if (token.match(/^\}/) && (depth == 1)) {
                        if (!args.length) {
                            var cmd = building.match(/^(@)(?!replace)(.+)$/) || building.match(/^(\w+)(?:\s+(\w+))?\s*$/);
                            if (cmd && cmd[0]) {args['command'] = cmd[0]}
                            if (cmd && cmd[1]) {args['firstarg'] = cmd[1]}
                        }
                        args.push(building);
                        building = '';
                        args.stringform = command;
                        args.toString = function() { return this.stringform; };
                        if (squiffy.ui.textProcessorFunctions[args.cmd]) {
                            output += squiffy.ui.textProcessorFunctions[cmdname].apply(args, args) || '';
                        } else {
                            output += command + '}';
                        }
                        command = '';
                        output += token.substr(1);
                        depth = 0;
                    } else if (token.match(/^:/) && (depth == 1)) {
                        command += token;
                        args.push(building);
                        building = token.substr(1);
                    } else {
                        if (token.match(/^\{/)) {depth++;}
                        if (token.match(/^\}/)) {depth--;}
                        building += token;
                        command += token;
                    }
                } else if(token.match(/^\{/)) {
                    building = token.substr(1);
                    args = [];
                    depth = 1;
                    command = token;
                } else {
                    output += token;
                }
            });
            if (command) {
                output += command;
            }
            return processtextoriginal(output);
        };
        squiffy.ui.textProcessorFunctions = {
            random: function (command, ...options) {
                var result = options ? squiffy.ui.processText(options[Math.floor(Math.random() * options.length)], this.data) : '';
                if (this.firstarg) { squiffy.set(this.firstarg, result); }
                return result;
            },
            // This lets you do things like {eval:$a + $b}
            // or "You give him $5, and have ${eval money:$money - 5} left."
            // attributes are preceded with $ rather than @ because the expression is javascript,
            // and JS variable names can't start with @
            eval: function (command, args) {
                var target, statement;
                if (this.firstarg && !args.length) {
                    statement = this.firstarg;
                } else {
                    target = this.firstarg;
                    statement = args.join(':');
                }
                var attributes = [undefined];
                var values = [];
                var test;
                $.each(statement.match(/(?<!\w)\$\w+/g), (i, term) => {
                    if (test = squiffy.get(term.substr(1))) {attributes.push(term); values.push(test);}
                });
                attributes.push(statement);
                var result = (new (Function.prototype.bind.apply(Function, attributes))).apply(undefined, values);
                if (target) { squiffy.set(target, result); }
                return result;
            },

            // Would probably be better to copy the code for 'if', 'else', 'rotate', and so on here as well.

        };
    }.toString();
    squiffy.set('_transitioncode', transition);
    squiffy.set('_transition', transition);
    squiffy.set('_output', ' ');
    squiffy.story.load();
    squiffy.story.go("firstsection");

With this in place, you could do something like:

The NPC sneaks up and steals {eval stolen:Math.floor(Math.random() * $money)} gold pieces from your pocket, leaving you with only {eval money:$money - $stolen}! You promptly chase after him and demand your money back.

"I'm feeling generous," he says. "If you can guess a coin toss, I'll give you your money back." Then he flips the coin.

[[Heads!]](cointoss,guess=heads)
[[Tails!]](cointoss,guess=tails)

[[cointoss]]:
You shout out "{guess}!" just as the coin lands, showing its {random toss:heads:tails} side on top.

{if guess=@toss:"Well, you win. I'm a man of my word," he says, and hands your money back.{@money+@stolen}}{else:"Tough luck, sucker."} Then he walks away without another word.

I may have to consider this for my game in development. Just released it - space flight and orbital mechanics. So much more to do...


This topic is now closed. Topics are closed after 180 days of inactivity.

Support

Forums