Hacking squiffy further…

mrangel
06 Feb 2021, 01:33

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.


DaxAtDS9
07 Feb 2021, 05:43

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


mrangel
07 Feb 2021, 11:10

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)


mrangel
08 Feb 2021, 15:00

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.

Bluevoss
08 Feb 2021, 17:55

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