@bitsy/hecks
Version:
a collection of re-usable scripts for bitsy game maker
263 lines (229 loc) • 9.23 kB
JavaScript
/**
@file kitsy-script-toolkit
@summary makes it easier and cleaner to run code before and after Bitsy functions or to inject new code into Bitsy script tags
@license WTFPL (do WTF you want)
@version 4.0.0
@requires Bitsy Version: 4.5, 4.6
@author @mildmojo
@description
HOW TO USE:
import {before, after, inject, addDialogTag, addDeferredDialogTag} from "./helpers/kitsy-script-toolkit";
before(targetFuncName, beforeFn);
after(targetFuncName, afterFn);
inject(searchRegex, replaceString);
addDialogTag(tagName, dialogFn);
addDeferredDialogTag(tagName, dialogFn);
For more info, see the documentation at:
https://github.com/seleb/bitsy-hacks/wiki/Coding-with-kitsy
*/
import bitsy from "bitsy";
import {
unique,
inject as utilsInject
} from "./utils";
// Ex: inject(/(names.sprite.set\( name, id \);)/, '$1console.dir(names)');
export function inject(searchRegex, replaceString) {
var kitsy = kitsyInit();
kitsy.queuedInjectScripts.push({
searchRegex: searchRegex,
replaceString: replaceString
});
}
// Ex: before('load_game', function run() { alert('Loading!'); });
// before('show_text', function run(text) { return text.toUpperCase(); });
// before('show_text', function run(text, done) { done(text.toUpperCase()); });
export function before(targetFuncName, beforeFn) {
var kitsy = kitsyInit();
kitsy.queuedBeforeScripts[targetFuncName] = kitsy.queuedBeforeScripts[targetFuncName] || [];
kitsy.queuedBeforeScripts[targetFuncName].push(beforeFn);
}
// Ex: after('load_game', function run() { alert('Loaded!'); });
export function after(targetFuncName, afterFn) {
var kitsy = kitsyInit();
kitsy.queuedAfterScripts[targetFuncName] = kitsy.queuedAfterScripts[targetFuncName] || [];
kitsy.queuedAfterScripts[targetFuncName].push(afterFn);
}
function kitsyInit() {
// return already-initialized kitsy
if (bitsy.kitsy) {
return bitsy.kitsy;
}
// Initialize kitsy
bitsy.kitsy = {
queuedInjectScripts: [],
queuedBeforeScripts: {},
queuedAfterScripts: {}
};
var oldStartFunc = bitsy.startExportedGame;
bitsy.startExportedGame = function doAllInjections() {
// Only do this once.
bitsy.startExportedGame = oldStartFunc;
// Rewrite scripts and hook everything up.
doInjects();
applyAllHooks();
// Start the game
bitsy.startExportedGame.apply(this, arguments);
};
return bitsy.kitsy;
}
function doInjects() {
bitsy.kitsy.queuedInjectScripts.forEach(function (injectScript) {
utilsInject(injectScript.searchRegex, injectScript.replaceString);
});
_reinitEngine();
}
function applyAllHooks() {
var allHooks = unique(Object.keys(bitsy.kitsy.queuedBeforeScripts).concat(Object.keys(bitsy.kitsy.queuedAfterScripts)));
allHooks.forEach(applyHook);
}
function applyHook(functionName) {
var functionNameSegments = functionName.split('.');
var obj = bitsy;
while (functionNameSegments.length > 1) {
obj = obj[functionNameSegments.shift()];
}
var lastSegment = functionNameSegments[0];
var superFn = obj[lastSegment];
var superFnLength = superFn ? superFn.length : 0;
var functions = [];
// start with befores
functions = functions.concat(bitsy.kitsy.queuedBeforeScripts[functionName] || []);
// then original
if (superFn) {
functions.push(superFn);
}
// then afters
functions = functions.concat(bitsy.kitsy.queuedAfterScripts[functionName] || []);
// overwrite original with one which will call each in order
obj[lastSegment] = function () {
var returnVal;
var args;
var i = 0;
function runBefore() {
// All outta functions? Finish
if (i === functions.length) {
return returnVal;
}
// Update args if provided.
if (arguments.length > 0) {
args = [].slice.call(arguments);
}
if (functions[i].length > superFnLength) {
// Assume funcs that accept more args than the original are
// async and accept a callback as an additional argument.
return functions[i++].apply(this, args.concat(runBefore.bind(this)));
} else {
// run synchronously
returnVal = functions[i++].apply(this, args);
if (returnVal && returnVal.length) {
args = returnVal;
}
return runBefore.apply(this, args);
}
}
return runBefore.apply(this, arguments);
};
}
function _reinitEngine() {
// recreate the script and dialog objects so that they'll be
// referencing the code with injections instead of the original
bitsy.scriptModule = new bitsy.Script();
bitsy.scriptInterpreter = bitsy.scriptModule.CreateInterpreter();
bitsy.dialogModule = new bitsy.Dialog();
bitsy.dialogRenderer = bitsy.dialogModule.CreateRenderer();
bitsy.dialogBuffer = bitsy.dialogModule.CreateBuffer();
}
// Rewrite custom functions' parentheses to curly braces for Bitsy's
// interpreter. Unescape escaped parentheticals, too.
export function convertDialogTags(input, tag) {
return input
.replace(new RegExp('\\\\?\\((' + tag + '(\\s+(".+?"|.+?))?)\\\\?\\)', 'g'), function(match, group){
if(match.substr(0,1) === '\\') {
return '('+ group + ')'; // Rewrite \(tag "..."|...\) to (tag "..."|...)
}
return '{'+ group + '}'; // Rewrite (tag "..."|...) to {tag "..."|...}
});
}
function addDialogFunction(tag, fn) {
var kitsy = kitsyInit();
kitsy.dialogFunctions = kitsy.dialogFunctions || {};
if (kitsy.dialogFunctions[tag]) {
throw new Error('The dialog function "' + tag + '" already exists.');
}
// Hook into game load and rewrite custom functions in game data to Bitsy format.
before('parseWorld', function (game_data) {
return [convertDialogTags(game_data, tag)];
});
kitsy.dialogFunctions[tag] = fn;
}
/**
* Adds a custom dialog tag which executes the provided function.
* For ease-of-use with the bitsy editor, tags can be written as
* (tagname "parameters") in addition to the standard {tagname "parameters"}
*
* Function is executed immediately when the tag is reached.
*
* @param {string} tag Name of tag
* @param {Function} fn Function to execute, with signature `function(environment, parameters, onReturn){}`
* environment: provides access to SetVariable/GetVariable (among other things, see Environment in the bitsy source for more info)
* parameters: array containing parameters as string in first element (i.e. `parameters[0]`)
* onReturn: function to call with return value (just call `onReturn(null);` at the end of your function if your tag doesn't interact with the logic system)
*/
export function addDialogTag(tag, fn) {
addDialogFunction(tag, fn);
inject(
/(var functionMap = new Map\(\);)/,
'$1functionMap.set("' + tag + '", kitsy.dialogFunctions.' + tag + ');'
);
}
/**
* Adds a custom dialog tag which executes the provided function.
* For ease-of-use with the bitsy editor, tags can be written as
* (tagname "parameters") in addition to the standard {tagname "parameters"}
*
* Function is executed after the dialog box.
*
* @param {string} tag Name of tag
* @param {Function} fn Function to execute, with signature `function(environment, parameters){}`
* environment: provides access to SetVariable/GetVariable (among other things, see Environment in the bitsy source for more info)
* parameters: array containing parameters as string in first element (i.e. `parameters[0]`)
*/
export function addDeferredDialogTag(tag, fn) {
addDialogFunction(tag, fn);
bitsy.kitsy.deferredDialogFunctions = bitsy.kitsy.deferredDialogFunctions || {};
var deferred = bitsy.kitsy.deferredDialogFunctions[tag] = [];
inject(
/(var functionMap = new Map\(\);)/,
'$1functionMap.set("' + tag + '", function(e, p, o){ kitsy.deferredDialogFunctions.' + tag + '.push({e:e,p:p}); o(null); });'
);
// Hook into the dialog finish event and execute the actual function
after('onExitDialog', function () {
while (deferred.length) {
var args = deferred.shift();
bitsy.kitsy.dialogFunctions[tag](args.e, args.p, args.o);
}
});
// Hook into the game reset and make sure data gets cleared
after('clearGameData', function () {
deferred.length = 0;
});
}
/**
* Adds two custom dialog tags which execute the provided function,
* one with the provided tagname executed after the dialog box,
* and one suffixed with 'Now' executed immediately when the tag is reached.
*
* i.e. helper for the (exit)/(exitNow) pattern.
*
* @param {string} tag Name of tag
* @param {Function} fn Function to execute, with signature `function(environment, parameters){}`
* environment: provides access to SetVariable/GetVariable (among other things, see Environment in the bitsy source for more info)
* parameters: array containing parameters as string in first element (i.e. `parameters[0]`)
*/
export function addDualDialogTag(tag, fn) {
addDialogTag(tag + 'Now', function(environment, parameters, onReturn) {
fn(environment, parameters);
onReturn(null);
});
addDeferredDialogTag(tag, fn);
}