Hacking squiffy further…
mrangel
06 Feb 2021, 01:33I 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.
![](https://i.imgur.com/qLCkdtBb.png)
DaxAtDS9
07 Feb 2021, 05:43Didnt get what it may be good for. Do you have any example?
mrangel
07 Feb 2021, 11:10Didnt 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:00OK… 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.
![](https://i.imgur.com/6mfIIbhb.gif)
Bluevoss
08 Feb 2021, 17:55I may have to consider this for my game in development. Just released it - space flight and orbital mechanics. So much more to do...