@bitsy/hecks
Version:
a collection of re-usable scripts for bitsy game maker
410 lines (357 loc) ⢠13.2 kB
JavaScript
/**
š
°
@file custom text effect
@summary make {custom}text effects{custom}
@license MIT
@version 2.1.1
@requires 5.3
@author Sean S. LeBlanc
@description
Adds support for a custom text effect
e.g. "normal text {my-effect}custom wavy text{my-effect}"
Multiple text effects can be added this way.
Without the hack, the game will still run normally since
bitsy just ignores text tags that aren't supported.
Because the dialog system uses private variables,
this one does some silly things with code injection.
HOW TO USE:
1. Copy-paste this script into a script tag after the bitsy source
2. Update the `hackOptions` object at the top of the script with your custom effects
TEXT EFFECT NOTES:
Each effect looks like:
key: function() {
this.DoEffect = function (char, time) {
// effect code
}
}
The key is the text you'll write inside {} in bitsy to trigger the effect
`this.DoEffect` is called every frame for the characters the effect is applied to
The first argument is `char`, an individual character, which has the following properties:
offset: offset from actual position in pixels. starts at {x:0, y:0}
color: color of rendered text in [0-255]. starts at {r:255, g:255, b:255, a:255}
bitmap: character bitmap as array of pixels
row: vertical position in rows (doesn't affect rendering)
col: horizontal position in characters (doesn't affect rendering)
`row`, `col`, and `offset` are reset every frame
`color` and any custom properties are reset when the dialog page is changed
`bitmap` is not reset! This edits the character in the font data directly
A few helpers are provided under `window.customTextEffects` for more complex effects:
- `saveOriginalChar`: saves the character string on `char`
- `setBitmap`: sets bitmap based on a new character
- `editBitmapCopy`: copies the character bitmap and runs an edit function once
The second argument is `time`, which is the time in milliseconds
A number of example effects are included
*/
this.hacks = this.hacks || {};
this.hacks.custom_text_effect = (function (exports,bitsy) {
;
var hackOptions = {
"my-effect": function () {
// a horizontal wavy effect with a blue tint
this.DoEffect = function (char, time) {
char.offset.x += 5 * Math.sin(time / 100 + char.col / 3);
char.color.r = 255 * Math.cos(time / 100 + char.col / 3);
};
},
droop: function () {
// causes text to droop down slowly over time
// note that it's adding a custom property to the character if it doesn't already exist
this.DoEffect = function (char, time) {
char.start = char.start || time;
char.offset.y += (time - char.start) / 100 * Math.abs(Math.sin(char.col));
};
},
fadeout: function () {
// fades text to invisible after appearing
this.DoEffect = function (char, time) {
char.start = char.start || time;
char.color.a = Math.max(0, 255 - (time - char.start) / 2);
};
},
noise: function () {
// renders noise on top of text
// note that it's making a copy with `.slice()` since it's a dynamic bitmap change
this.DoEffect = function (char) {
char.bitmap = char.bitmap.slice();
for(var i = 0; i < char.bitmap.length; ++i) {
char.bitmap[i] = Math.random() < 0.25 ? 1 : 0;
}
};
},
strike: function () {
// renders text with a strike-through
// note that it's using `editBitmapCopy` since it's a static bitmap change
this.DoEffect = function (char) {
var font = window.fontManager.Get(window.fontName);
var w = font.getWidth();
var h = font.getHeight();
window.customTextEffects.editBitmapCopy(char, function(bitmap) {
for(var x = 0; x < w; ++x) {
bitmap[x + Math.floor(h/2)*w] = 1;
}
});
};
},
scramble: function () {
// animated text scrambling
// note that it's saving the original character with `saveOriginalChar` so `char.original` can be used
// it's also using `setBitmap` to render a different character in the font
this.DoEffect = function (char, time) {
window.customTextEffects.saveOriginalChar(char);
if (char.original.match(/\s|\0/)) {
return;
}
var c = String.fromCharCode(char.original.codePointAt(0) + (char.col + time / 40) % 10);
window.customTextEffects.setBitmap(char, c);
};
},
rot13: function () {
// puts letters through the rot13 cipher (see www.rot13.com)
this.DoEffect = function (char) {
window.customTextEffects.saveOriginalChar(char);
var c = char.original.replace(/[a-z]/, function (c) {
return String.fromCharCode((c.codePointAt(0) - 97 + 13) % 26 + 97);
}).replace(/[A-Z]/, function (c) {
return String.fromCharCode((c.codePointAt(0) - 65 + 13) % 26 + 65);
});
window.customTextEffects.setBitmap(char, c);
};
},
sponge: function () {
// animated alternating letter case
// note that it's using a locally defined function
function posmod(a, b) {
return ((a % b) + b) % b;
}
this.DoEffect = function (char, time) {
window.customTextEffects.saveOriginalChar(char);
var c = char.original[['toUpperCase', 'toLowerCase'][Math.round(posmod(time / 1000 - (char.col + char.row) / 2, 1))]]();
window.customTextEffects.setBitmap(char, c);
};
},
flag: function () {
// applies a wave effect that's more intense towards the ends of words
// note that it's using function scope variables to track state across
// multiple letters in order to figure out where words begin
var lastSpace = 0;
var lastCol = -Infinity;
this.DoEffect = function (char, time) {
window.customTextEffects.saveOriginalChar(char);
if (char.original.match(/\s|\0/)) {
return;
} else if (Math.abs(char.col - lastCol) > 1) {
lastSpace = char.col - 1;
}
lastCol = char.col;
char.offset.y -= Math.pow(char.col - lastSpace, 1.5) * (Math.sin(time / 120 + char.col / 2));
};
}
};
bitsy = bitsy && bitsy.hasOwnProperty('default') ? bitsy['default'] : bitsy;
/**
@file utils
@summary miscellaneous bitsy utilities
@author Sean S. LeBlanc
*/
/*
Helper used to replace code in a script tag based on a search regex
To inject code without erasing original string, using capturing groups; e.g.
inject(/(some string)/,'injected before $1 injected after')
*/
function inject(searchRegex, replaceString) {
// find the relevant script tag
var scriptTags = document.getElementsByTagName('script');
var scriptTag;
var code;
for (var i = 0; i < scriptTags.length; ++i) {
scriptTag = scriptTags[i];
var matchesSearch = scriptTag.textContent.search(searchRegex) !== -1;
var isCurrentScript = scriptTag === document.currentScript;
if (matchesSearch && !isCurrentScript) {
code = scriptTag.textContent;
break;
}
}
// error-handling
if (!code) {
throw 'Couldn\'t find "' + searchRegex + '" in script tags';
}
// modify the content
code = code.replace(searchRegex, replaceString);
// replace the old script tag with a new one using our modified code
var newScriptTag = document.createElement('script');
newScriptTag.textContent = code;
scriptTag.insertAdjacentElement('afterend', newScriptTag);
scriptTag.remove();
}
/**
* Helper for getting an array with unique elements
* @param {Array} array Original array
* @return {Array} Copy of array, excluding duplicates
*/
function unique(array) {
return array.filter(function (item, idx) {
return array.indexOf(item) === idx;
});
}
/**
@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
*/
// Ex: inject(/(names.sprite.set\( name, id \);)/, '$1console.dir(names)');
function inject$1(searchRegex, replaceString) {
var kitsy = kitsyInit();
kitsy.queuedInjectScripts.push({
searchRegex: searchRegex,
replaceString: replaceString
});
}
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) {
inject(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();
}
// custom text effects are injected,
// so need to store helpers somewhere accessible
// from outside of hack scope
window.customTextEffects = {
// helper for caching original character string on character object
saveOriginalChar: function (char) {
if (char.original !== undefined) {
return;
}
var font = window.fontManager.Get(window.fontName);
var characters = Object.entries(font.getData());
var character = characters.find(function(keyval){
return keyval[1].toString() === char.bitmap.toString();
});
char.original = String.fromCharCode(character[0]);
},
// helper for setting new character bitmap by string on character object
setBitmap: function (char, c) {
char.bitmap = window.fontManager.Get(window.fontName).getChar(c);
},
// helper for editing bitmap without affecting other characters
editBitmapCopy: function (char, editFn) {
if (char.originalBitmap !== undefined) {
return;
}
char.originalBitmap = char.bitmap;
char.bitmap = char.bitmap.slice();
editFn(char.bitmap);
}
};
// generate code for each text effect
var functionMapCode = '';
var textEffectCode = '';
for (var i in hackOptions) {
if (hackOptions.hasOwnProperty(i)) {
functionMapCode += 'functionMap.set("' + i + '", function (environment, parameters, onReturn) {addOrRemoveTextEffect(environment, "' + i + '");onReturn(null);});';
textEffectCode += 'TextEffects["' + i + '"] = new (' + hackOptions[i].toString() + ')();';
}
}
// inject custom text effect code
inject$1(/(var functionMap = new Map\(\);)/, '$1' + functionMapCode);
inject$1(/(var TextEffects = new Map\(\);)/, '$1' + textEffectCode);
exports.hackOptions = hackOptions;
return exports;
}({},window));