UNPKG

@bitsy/hecks

Version:

a collection of re-usable scripts for bitsy game maker

856 lines (736 loc) • 26.6 kB
/** šŸ”„ @file online @summary multiplayer bitsy @license MIT @version 2.1.4 @requires 5.5 @author Sean S. LeBlanc @description Provides the groundwork for running a small online multiplayer bitsy game. Running it requires running a copy of this server: https://github.com/seleb/web-rtc-mesh Server notes: - The actual game data is sent using peer-to-peer data channels; the server just hosts client code and negotaties initial connections. (i.e. it uses very little bandwidth) - A single server can host multiple games simultaneously - If you're not sure how to setup/use the server, ask for help! This hack also includes the hacks for editing images/dialog at runtime through dialog. This provides the (image), (imageNow), (imagePal), (imagePalNow), and (dialog) commands. In the online hack, these will automatically trigger a sprite update so that updates to the avatar will be reflected for other players. See the respective hacks for more info on how to use the commands. Note on dialog: You can use scripting in the dialog, but it will execute on the other players' games, accessing *their* variables. HOW TO USE: 1. Copy-paste this script into a script tag after the bitsy source 2. Edit `hackOptions.host` below to point to your server (depending on hosting, you may need to use `ws://` instead of `wss://`) 3. Edit other hackOptions as needed */ this.hacks = this.hacks || {}; this.hacks.online = (function (exports,bitsy) { 'use strict'; var hackOptions = { host: "wss://your signalling server", // room: "custom room", // sets the room on the server to use; otherwise, uses game title immediateMode: true, // if true, teleports players to their reported positions; otherwise, queues movements and lets bitsy handle the walking (note: other players pick up items like this) ghosts: false, // if true, sprites from players who disconnected while you were online won't go away until you restart debug: false, // if true, includes web-rtc-mesh debug logs in console }; 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 image by name or id Args: name: id or name of image to return map: map of images (e.g. `sprite`, `tile`, `item`) Returns: the image in the given map with the given name/id */ function getImage(name, map) { var id = map.hasOwnProperty(name) ? name : Object.keys(map).find(function (e) { return map[e].name == name; }); return map[id]; } /** * 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 }); } // 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()); }); 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!'); }); 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) { 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(); } // Rewrite custom functions' parentheses to curly braces for Bitsy's // interpreter. Unescape escaped parentheticals, too. 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) */ function addDialogTag(tag, fn) { addDialogFunction(tag, fn); inject$1( /(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]`) */ function addDeferredDialogTag(tag, fn) { addDialogFunction(tag, fn); bitsy.kitsy.deferredDialogFunctions = bitsy.kitsy.deferredDialogFunctions || {}; var deferred = bitsy.kitsy.deferredDialogFunctions[tag] = []; inject$1( /(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]`) */ function addDualDialogTag(tag, fn) { addDialogTag(tag + 'Now', function(environment, parameters, onReturn) { fn(environment, parameters); onReturn(null); }); addDeferredDialogTag(tag, fn); } /** ā˜• @file javascript dialog @summary execute arbitrary javascript from dialog @license MIT @version 3.2.2 @requires Bitsy Version: 4.5, 4.6 @author Sean S. LeBlanc @description Lets you execute arbitrary JavaScript from dialog (including inside conditionals). If you're familiar with the Bitsy source, this will let you write one-shot hacks for a wide variety of situations. Usage: (js "<JavaScript code to evaluate after dialog is closed>") (jsNow "<JavaScript code to evaluate immediately>") Examples: move a sprite: (js "sprite['a'].x = 10;") edit palette colour: (js "getPal(curPal())[0] = [255,0,0];renderImages();") place an item next to player: (js "room[curRoom].items.push({id:'0',x:player().x+1,y:player().y});") verbose facimile of exit-from-dialog: (js "var _onExitDialog=onExitDialog;onExitDialog=function(){player().room=curRoom='3';_onExitDialog.apply(this,arguments);onExitDialog=_onExitDialog;};") HOW TO USE: 1. Copy-paste into a script tag after the bitsy source 2. Add (js "<code>") to your dialog as needed NOTE: This uses parentheses "()" instead of curly braces "{}" around function calls because the Bitsy editor's fancy dialog window strips unrecognized curly-brace functions from dialog text. To keep from losing data, write these function calls with parentheses like the examples above. For full editor integration, you'd *probably* also need to paste this code at the end of the editor's `bitsy.js` file. Untested. */ var indirectEval$1 = eval; function executeJs(environment, parameters) { indirectEval$1(parameters[0]); } addDualDialogTag('js', executeJs); /** @file edit image at runtime @summary API for updating image data at runtime. @author Sean S. LeBlanc @description Adds API for updating sprite, tile, and item data at runtime. Individual frames of image data in bitsy are 8x8 1-bit 2D arrays in yx order e.g. the default player is: [ [0,0,0,1,1,0,0,0], [0,0,0,1,1,0,0,0], [0,0,0,1,1,0,0,0], [0,0,1,1,1,1,0,0], [0,1,1,1,1,1,1,0], [1,0,1,1,1,1,0,1], [0,0,1,0,0,1,0,0], [0,0,1,0,0,1,0,0] ] */ /* Args: id: string id or name frame: animation frame (0 or 1) map: map of images (e.g. `sprite`, `tile`, `item`) Returns: a single frame of a image data */ function getImageData(id, frame, map) { return bitsy.renderer.GetImageSource(getImage(id, map).drw)[frame]; } /* Updates a single frame of image data Args: id: string id or name frame: animation frame (0 or 1) map: map of images (e.g. `sprite`, `tile`, `item`) newData: new data to write to the image data */ function setImageData(id, frame, map, newData) { var drawing = getImage(id, map); var drw = drawing.drw; var img = bitsy.renderer.GetImageSource(drw).slice(); img[frame] = newData; bitsy.renderer.SetImageSource(drw, img); } function setSpriteData(id, frame, newData) { setImageData(id, frame, bitsy.sprite, newData); } /** šŸ–Œ @file edit image from dialog @summary edit sprites, items, and tiles from dialog @license MIT @version 1.2.3 @requires 5.3 @author Sean S. LeBlanc @description You can use this to edit the image data of sprites (including the player avatar), items, and tiles through dialog. Image data can be replaced with data from another image, and the palette index can be set. (image "map, target, source") Parameters: map: Type of image (SPR, TIL, or ITM) target: id/name of image to edit source: id/name of image to copy (imageNow "map, target, source") Same as (image), but applied immediately instead of after dialog is closed. (imagePal "map, target, palette") Parameters: map: Type of image (SPR, TIL, or ITM) target: id/name of image to edit source: palette index (0 is bg, 1 is tiles, 2 is sprites/items, anything higher requires editing your game data to include more) (imagePalNow "map, target, palette") Same as (imagePal), but applied immediately instead of after dialog is closed. Examples: (image "SPR, A, a") (imageNow "TIL, a, floor") (image "ITM, a, b") (imagePal "SPR, A, 1") (imagePalNow "TIL, floor, 2") HOW TO USE: 1. Copy-paste this script into a new script tag after the Bitsy source code. It should appear *before* any other mods that handle loading your game data so it executes *after* them (last-in first-out). TIPS: - The player avatar is always a sprite with id "A"; you can edit your gamedata to give them a name for clarity - You can use the full names or shorthand of image types (e.g. "SPR" and "sprite" will both work) - The "source" images don't have to be placed anywhere; so long as they exist in the gamedata they'll work - This is a destructive operation! Unless you have a copy of an overwritten image, you won't be able to get it back during that run NOTE: This uses parentheses "()" instead of curly braces "{}" around function calls because the Bitsy editor's fancy dialog window strips unrecognized curly-brace functions from dialog text. To keep from losing data, write these function calls with parentheses like the examples above. For full editor integration, you'd *probably* also need to paste this code at the end of the editor's `bitsy.js` file. Untested. */ // map of maps var maps; after('load_game', function () { maps = { spr: bitsy.sprite, sprite: bitsy.sprite, til: bitsy.tile, tile: bitsy.tile, itm: bitsy.item, item: bitsy.item, }; }); function editImage(environment, parameters) { var i; // parse parameters var params = parameters[0].split(/,\s?/); params[0] = (params[0] || "").toLowerCase(); var mapId = params[0]; var tgtId = params[1]; var srcId = params[2]; if (!mapId || !tgtId || !srcId) { throw new Error('Image expects three parameters: "map, target, source", but received: "' + params.join(', ') + '"'); } // get objects var mapObj = maps[mapId]; if (!mapObj) { throw new Error('Invalid map "' + mapId + '". Try "SPR", "TIL", or "ITM" instead.'); } var tgtObj = getImage(tgtId, mapObj); if (!tgtObj) { throw new Error('Target "' + tgtId + '" was not the id/name of a ' + mapId + '.'); } var srcObj = getImage(srcId, mapObj); if (!srcObj) { throw new Error('Source "' + srcId + '" was not the id/name of a ' + mapId + '.'); } // copy animation from target to source tgtObj.animation = { frameCount: srcObj.animation.frameCount, isAnimated: srcObj.animation.isAnimated, frameIndex: srcObj.animation.frameIndex }; for (i = 0; i < srcObj.animation.frameCount; ++i) { setImageData(tgtId, i, mapObj, getImageData(srcId, i, mapObj)); } } function editPalette(environment, parameters) { // parse parameters var params = parameters[0].split(/,\s?/); params[0] = (params[0] || "").toLowerCase(); var mapId = params[0]; var tgtId = params[1]; var palId = params[2]; if (!mapId || !tgtId || !palId) { throw new Error('Image expects three parameters: "map, target, palette", but received: "' + params.join(', ') + '"'); } // get objects var mapObj = maps[mapId]; if (!mapObj) { throw new Error('Invalid map "' + mapId + '". Try "SPR", "TIL", or "ITM" instead.'); } var tgtObj = getImage(tgtId, mapObj); if (!tgtObj) { throw new Error('Target "' + tgtId + '" was not the id/name of a ' + mapId + '.'); } var palObj = parseInt(palId); if (isNaN(palObj)) { throw new Error('Palette "' + palId + '" was not a number.'); } // set palette tgtObj.col = palObj; // update images in cache bitsy.renderImageForAllPalettes(tgtObj); } // hook up the dialog tags addDualDialogTag('image', editImage); addDualDialogTag('imagePal', editPalette); /** šŸ“ @file edit dialog from dialog @summary edit dialog from dialog (yes really) @license MIT @version 1.1.1 @author Sean S. LeBlanc @description You can use this to edit the dialog of sprites/items through dialog. (dialog "map, target, newDialog") Parameters: map: Type of image (SPR or ITM) target: id/name of image to edit newDialog: id/name of image to edit Note: this hack disables bitsy's script caching. HOW TO USE: Copy-paste this script into a new script tag after the Bitsy source code. TIPS: - The player avatar is always a sprite with id "A"; you can edit your gamedata to give them a name for clarity - You can use the full names or shorthand of image types (e.g. "SPR" and "sprite" will both work) */ // map of maps var maps$1; after('load_game', function () { maps$1 = { spr: bitsy.sprite, sprite: bitsy.sprite, itm: bitsy.item, item: bitsy.item, }; }); function editDialog(environment, parameters) { // parse parameters var params = parameters[0].split(/,\s?/); params[0] = (params[0] || "").toLowerCase(); var mapId = params[0]; var tgtId = params[1]; var newDialog = params[2] || ""; if (!mapId || !tgtId) { throw new Error('Image expects three parameters: "map, target, newDialog", but received: "' + params.join(', ') + '"'); } // get objects var mapObj = maps$1[mapId]; if (!mapObj) { throw new Error('Invalid map "' + mapId + '". Try "SPR" or "ITM" instead.'); } var tgtObj = getImage(tgtId, mapObj); if (!tgtObj) { throw new Error('Target "' + tgtId + '" was not the id/name of a ' + mapId + '.'); } bitsy.dialog[tgtObj.dlg] = newDialog; } // hook up the dialog tag addDeferredDialogTag('dialog', editDialog); // disable bitsy's dialog caching inject(/startDialog\(dialogStr,dialogId\);/g, 'startDialog(dialogStr);'); // download the client script // bitsy starts onload, so adding it to the head // is enough to delay game startup until it's loaded/errored var clientScript = document.createElement("script"); clientScript.src = hackOptions.host.replace(/^ws/, "http") + "/client.js"; clientScript.onload = function () { console.log("online available!"); }; clientScript.onerror = function (error) { console.error("online not available!", error); }; document.head.appendChild(clientScript); var client; function onData(event) { var spr; var data = event.data; switch (data.e) { case "move": spr = bitsy.sprite[event.from]; if (spr) { // move sprite if (hackOptions.immediateMode) { // do it now spr.x = data.x; spr.y = data.y; spr.room = data.room; } else { // let bitsy handle it later spr.walkingPath.push({ x: data.x, y: data.y }); } } else { // got a move from an unknown player, // so ask them who they are client.send(event.from, { e: "gimmeSprite" }); } break; case "gimmeSprite": // send a sprite update to specific peer client.send(event.from, getSpriteUpdate()); break; case "sprite": // update a sprite var longname = "SPR_" + event.from; spr = bitsy.sprite[event.from] = { animation: { frameCount: data.data.length, frameIndex: 0, isAnimated: data.data.length > 1 }, col: data.col, dlg: longname, drw: longname, inventory: {}, name: event.from, walkingPath: [], x: data.x, y: data.y, room: data.room }; bitsy.dialog[longname] = data.dlg; bitsy.renderer.SetImageSource(longname, data.data); for (var frame = 0; frame < data.data.length; ++frame) { setSpriteData(event.from, frame, data.data[frame]); } break; } } function onClose(event) { if (event.error) { console.error('Connection closed due to error:', event.error); } if (!hackOptions.ghosts) { delete bitsy.sprite[event.id]; } } after("startExportedGame", function () { if (!window.Client) { console.error("Couldn't retrieve client; running game offline"); } client = new window.Client.default({ host: hackOptions.host, room: hackOptions.room || bitsy.title, }); client.on(window.Client.DATA, onData); client.on(window.Client.CLOSE, onClose); client.setDebug(hackOptions.debug); }); after("movePlayer", moveSprite); after("onready", function () { // tell everyone who you are // and ask who they are 1s after starting setTimeout(function () { if (client) { updateSprite(); client.broadcast({ e: "gimmeSprite" }); } }, 1000); }); // tell everyone where you are function moveSprite() { var p = bitsy.player(); client.broadcast({ e: "move", x: p.x, y: p.y, room: p.room }); } // tell everyone who you are function updateSprite() { client.broadcast(getSpriteUpdate()); } // helper to create a sprite update based on the player avatar function getSpriteUpdate() { var p = bitsy.player(); return { e: "sprite", data: bitsy.renderer.GetImageSource(p.drw), x: p.x, y: p.y, room: p.room, dlg: bitsy.dialog[p.dlg], col: p.col }; } // trigger sprite updates after these dialog functions [ 'image', 'imageNow', 'imagePal', 'imagePalNow', 'dialog' ].forEach(function (tag) { var original = bitsy.kitsy.dialogFunctions[tag]; bitsy.kitsy.dialogFunctions[tag] = function () { original.apply(this, arguments); updateSprite(); }; }); exports.hackOptions = hackOptions; return exports; }({},window));