├── .gitignore ├── src ├── images │ ├── icon.xcf │ ├── logo.xcf │ ├── datacard.xcf │ └── arrow-key-right.xcf ├── runtime │ ├── vars.com.json │ ├── env.com.json │ ├── focus.com.json │ ├── resources.com.json │ ├── story.com.json │ ├── system.com.json │ ├── storage.com.json │ ├── nodes.com.json │ ├── audio.com.json │ ├── settings.com.json │ ├── browser-ui │ │ ├── highlighter.com.json │ │ ├── cartridges.com.json │ │ ├── options.com.json │ │ ├── screens.com.json │ │ ├── ui.com.json │ │ ├── options.com.js │ │ ├── highlighter.com.js │ │ └── cartridges.com.js │ ├── interpreter.com.json │ ├── resources.com.js │ ├── env.com.js │ ├── vars.com.js │ ├── focus.com.js │ ├── settings.com.js │ ├── story.com.js │ ├── system.com.js │ ├── storage.com.js │ ├── audio.com.js │ └── nodes.com.js ├── utils │ ├── browser │ │ ├── domUtils.com.json │ │ ├── positioning.com.json │ │ ├── revealEffect.com.json │ │ ├── confirmations.com.json │ │ ├── notifications.com.json │ │ ├── scrolling.com.json │ │ ├── domUtils.com.js │ │ ├── positioning.com.js │ │ ├── confirmations.com.js │ │ ├── notifications.com.js │ │ ├── scrolling.com.js │ │ └── revealEffect.com.js │ ├── logger.com.json │ ├── scripts.com.json │ ├── fileSystem.com.json │ ├── appCacheManifest.com.json │ ├── toothrotErrors.com.json │ ├── toothrotErrors.com.js │ ├── appCacheManifest.com.js │ ├── errors.json │ ├── logger.com.js │ ├── scripts.com.js │ └── fileSystem.com.js ├── validator.com.json ├── storyFileReader.com.json ├── initializer.com.json ├── parser.com.json ├── packer.com.json ├── gatherer.com.json ├── scriptExporter.com.json ├── builder.com.json ├── storyFileReader.com.js ├── initializer.com.js ├── packer.com.js ├── scriptExporter.com.js ├── gatherer.com.js ├── validator.com.js └── builder.com.js ├── resources ├── files │ ├── images │ │ └── logo.png │ ├── style │ │ ├── OpenSans-Bold.ttf │ │ ├── OpenSans-Light.ttf │ │ ├── OpenSans-Regular.ttf │ │ └── custom.css │ ├── index.js │ └── index.html └── resources │ ├── images │ └── datacard.png │ ├── extras.trsf.md │ ├── templates │ ├── notification.html │ ├── confirm.html │ └── ui.html │ ├── main.trsf.md │ └── screens │ ├── main.html │ ├── pause.html │ ├── settings.html │ └── cartridges.html ├── misc └── templates │ ├── component.js │ ├── toothrotResources.js │ ├── script.js │ ├── resources.js │ ├── runtime.js │ └── scripts.js ├── bin ├── cli.com.json ├── debug.com.json ├── commands │ ├── pack.com.json │ ├── build.com.json │ ├── init.com.json │ ├── build-desktop.com.json │ ├── parse.com.json │ ├── validate.com.json │ ├── init.com.js │ ├── build-desktop.com.js │ ├── build.com.js │ ├── pack.com.js │ ├── parse.com.js │ └── validate.com.js ├── cli.js ├── args.js ├── debug.com.js └── cli.com.js ├── LICENSE ├── package.json ├── README.md ├── CHANGELOG.md ├── index.js └── .eslintrc.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | -------------------------------------------------------------------------------- /src/images/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/src/images/icon.xcf -------------------------------------------------------------------------------- /src/images/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/src/images/logo.xcf -------------------------------------------------------------------------------- /src/images/datacard.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/src/images/datacard.xcf -------------------------------------------------------------------------------- /resources/files/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/resources/files/images/logo.png -------------------------------------------------------------------------------- /src/images/arrow-key-right.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/src/images/arrow-key-right.xcf -------------------------------------------------------------------------------- /resources/files/style/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/resources/files/style/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /resources/files/style/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/resources/files/style/OpenSans-Light.ttf -------------------------------------------------------------------------------- /resources/resources/images/datacard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/resources/resources/images/datacard.png -------------------------------------------------------------------------------- /resources/files/style/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toothrot-if/toothrot/HEAD/resources/files/style/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /resources/resources/extras.trsf.md: -------------------------------------------------------------------------------- 1 | 2 | ## another_section 3 | 4 | ### another_node 5 | 6 | This is a node in another section, written in a separate file. 7 | 8 | (<) 9 | -------------------------------------------------------------------------------- /resources/resources/templates/notification.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /misc/templates/component.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var mod = {}; 4 | 5 | (function (module) { 6 | '{{$content}}' 7 | }(mod)); 8 | 9 | return mod.exports; 10 | 11 | }()).create -------------------------------------------------------------------------------- /bin/cli.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli"], 8 | "offers": ["cli"], 9 | "file": "./cli.com.js" 10 | } -------------------------------------------------------------------------------- /resources/files/index.js: -------------------------------------------------------------------------------- 1 | /* global TOOTHROT */ 2 | 3 | (function () { 4 | 5 | // 6 | // Write your custom init code here. The variable `TOOTHROT` is a `multiversum` application. 7 | // 8 | 9 | // @ts-ignore 10 | TOOTHROT.init(); 11 | }()); 12 | -------------------------------------------------------------------------------- /resources/files/style/custom.css: -------------------------------------------------------------------------------- 1 | /* Use this file to add your own styles without changing the default one. */ 2 | 3 | .screen-container { 4 | background: linear-gradient(to bottom, rgba(230, 230, 230, 1) 0, 5 | rgba(140, 140, 140, 1) 30%, rgba(0, 0, 0, 1) 95%); 6 | } 7 | -------------------------------------------------------------------------------- /bin/debug.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cliDebug", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli"], 8 | "flags": ["debug"], 9 | "file": "./debug.com.js" 10 | } -------------------------------------------------------------------------------- /src/runtime/vars.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vars", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["any"], 8 | "offers": ["vars"], 9 | "file": "./vars.com.js" 10 | } -------------------------------------------------------------------------------- /src/runtime/env.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "env", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser", "cli"], 8 | "offers": ["env"], 9 | "file": "./env.com.js" 10 | } -------------------------------------------------------------------------------- /src/runtime/focus.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "focus", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["focus"], 9 | "file": "./focus.com.js" 10 | } -------------------------------------------------------------------------------- /bin/commands/pack.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli.commands.pack", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli"], 8 | "requires": ["packer"], 9 | "file": "./pack.com.js" 10 | } -------------------------------------------------------------------------------- /src/runtime/resources.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resources", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["any"], 8 | "offers": ["resources"], 9 | "file": "./resources.com.js" 10 | } -------------------------------------------------------------------------------- /bin/commands/build.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli.commands.build", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli"], 8 | "requires": ["builder"], 9 | "file": "./build.com.js" 10 | } -------------------------------------------------------------------------------- /bin/commands/init.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli.commands.init", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli"], 8 | "requires": ["initializer"], 9 | "file": "./init.com.js" 10 | } -------------------------------------------------------------------------------- /src/utils/browser/domUtils.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domUtils", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["domUtils"], 9 | "file": "./domUtils.com.js" 10 | } -------------------------------------------------------------------------------- /src/runtime/story.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "story", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["any"], 8 | "offers": ["story"], 9 | "requires": ["resources"], 10 | "file": "./story.com.js" 11 | } -------------------------------------------------------------------------------- /src/runtime/system.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "system", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["any"], 8 | "offers": ["system"], 9 | "modules": ["clone"], 10 | "file": "./system.com.js" 11 | } -------------------------------------------------------------------------------- /src/utils/browser/positioning.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uiPositioning", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["uiPositioning"], 9 | "file": "./positioning.com.js" 10 | } -------------------------------------------------------------------------------- /bin/commands/build-desktop.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli.commands.build-desktop", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli"], 8 | "requires": ["builder"], 9 | "file": "./build-desktop.com.js" 10 | } -------------------------------------------------------------------------------- /bin/commands/parse.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli.commands.parse", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli"], 8 | "requires": ["logger", "parser", "storyFileReader"], 9 | "file": "./parse.com.js" 10 | } -------------------------------------------------------------------------------- /src/runtime/storage.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storage", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["storage"], 9 | "requires": ["logger"], 10 | "file": "./storage.com.js" 11 | } -------------------------------------------------------------------------------- /bin/commands/validate.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli.commands.validate", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli"], 8 | "requires": ["logger", "parser", "storyFileReader"], 9 | "file": "./validate.com.js" 10 | } -------------------------------------------------------------------------------- /src/utils/logger.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logger", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build", "run"], 7 | "environments": ["cli", "browser", "node"], 8 | "offers": ["logger"], 9 | "modules": ["colors"], 10 | "file": "./logger.com.js" 11 | } -------------------------------------------------------------------------------- /src/validator.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validator", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli", "node"], 8 | "offers": ["validator"], 9 | "requires": ["toothrotErrors"], 10 | "file": "./validator.com.js" 11 | } -------------------------------------------------------------------------------- /src/utils/scripts.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["scripts"], 9 | "requires": ["env", "vars", "story", "logger"], 10 | "file": "./scripts.com.js" 11 | } -------------------------------------------------------------------------------- /src/runtime/nodes.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodes", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["any"], 8 | "offers": ["nodes"], 9 | "requires": ["env", "vars", "story"], 10 | "modules": ["clone"], 11 | "file": "./nodes.com.js" 12 | } -------------------------------------------------------------------------------- /src/utils/fileSystem.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fileSystem", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli", "browser", "node", "any"], 8 | "offers": ["fileSystem"], 9 | "modules": ["path"], 10 | "file": "./fileSystem.com.js" 11 | } -------------------------------------------------------------------------------- /src/runtime/audio.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["audio"], 9 | "requires": ["vars", "settings"], 10 | "modules": ["howler"], 11 | "file": "./audio.com.js" 12 | } -------------------------------------------------------------------------------- /src/storyFileReader.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storyFileReader", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli", "node"], 8 | "requires": ["fileSystem"], 9 | "offers": ["storyFileReader"], 10 | "file": "./storyFileReader.com.js" 11 | } -------------------------------------------------------------------------------- /src/utils/browser/revealEffect.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uiRevealEffect", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["uiRevealEffect"], 9 | "modules": ["transform-js"], 10 | "file": "./revealEffect.com.js" 11 | } -------------------------------------------------------------------------------- /src/runtime/settings.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "settings", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["any"], 8 | "offers": ["settings"], 9 | "requires": ["story", "storage"], 10 | "modules": ["clone"], 11 | "file": "./settings.com.js" 12 | } -------------------------------------------------------------------------------- /src/utils/browser/confirmations.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uiConfirmations", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["uiConfirmations"], 9 | "requires": ["resources", "focus"], 10 | "file": "./confirmations.com.js" 11 | } -------------------------------------------------------------------------------- /src/utils/browser/notifications.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uiNotifications", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["uiNotifications"], 9 | "modules": ["vrep", "transform-js"], 10 | "file": "./notifications.com.js" 11 | } -------------------------------------------------------------------------------- /src/initializer.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "initializer", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli", "node"], 8 | "offers": ["initializer"], 9 | "requires": ["logger", "fileSystem"], 10 | "modules": ["path"], 11 | "file": "./initializer.com.js" 12 | } -------------------------------------------------------------------------------- /src/parser.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parser", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli", "node"], 8 | "offers": ["parser"], 9 | "requires": ["validator", "toothrotErrors"], 10 | "modules": ["marked", "clone"], 11 | "file": "./parser.com.js" 12 | } -------------------------------------------------------------------------------- /src/utils/browser/scrolling.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uiScrolling", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["uiScrolling", "uiPositioning"], 9 | "modules": ["transform-js", "scrollbarwidth"], 10 | "file": "./scrolling.com.js" 11 | } -------------------------------------------------------------------------------- /src/packer.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packer", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli", "node"], 8 | "offers": ["packer"], 9 | "requires": ["parser", "storyFileReader"], 10 | "modules": ["fs", "datauri", "path"], 11 | "file": "./packer.com.js" 12 | } -------------------------------------------------------------------------------- /src/gatherer.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toothrotGatherer", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli", "node"], 8 | "offers": ["toothrotGatherer"], 9 | "modules": ["path", "vrep", "multiversum/host", "multiversum/gatherer"], 10 | "file": "./gatherer.com.js" 11 | } -------------------------------------------------------------------------------- /src/utils/appCacheManifest.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appCacheManifest", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["node"], 8 | "requires": ["fileSystem", "logger"], 9 | "offers": ["appCacheManifest"], 10 | "modules": ["path"], 11 | "file": "./appCacheManifest.com.js" 12 | } -------------------------------------------------------------------------------- /src/runtime/browser-ui/highlighter.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "highlighter", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["highlighter"], 9 | "requires": ["focus", "uiPositioning", "uiScrolling"], 10 | "modules": ["transform-js"], 11 | "file": "./highlighter.com.js" 12 | } -------------------------------------------------------------------------------- /src/runtime/browser-ui/cartridges.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cartridges", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["cartridges"], 9 | "requires": ["interpreter", "resources", "uiConfirmations"], 10 | "modules": ["png-cartridge"], 11 | "file": "./cartridges.com.js" 12 | } -------------------------------------------------------------------------------- /src/runtime/interpreter.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpreter", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["any"], 8 | "offers": ["interpreter"], 9 | "requires": ["env", "vars", "nodes", "focus", "story", "storage", "settings", "scripts"], 10 | "modules": ["clone", "deepmerge"], 11 | "file": "./interpreter.com.js" 12 | } -------------------------------------------------------------------------------- /resources/resources/main.trsf.md: -------------------------------------------------------------------------------- 1 | # My Story 2 | 3 | ## default 4 | 5 | (#) background: "linear-gradient(to bottom, rgba(230, 230, 230, 1) 0, rgba(140, 140, 140, 1) 30%, rgba(0, 0, 0, 1) 95%)" 6 | 7 | ### start 8 | 9 | My Story 10 | ================= 11 | 12 | Welcome to Toothrot Engine! 13 | 14 | Not sure how this stuff works? Check out the documentation section on our 15 | [official website](https://toothrot.one)! 16 | 17 | [Click here to go to another node.](#another_node) 18 | -------------------------------------------------------------------------------- /src/utils/toothrotErrors.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toothrotErrors", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build", "run"], 7 | "environments": ["cli", "browser", "node"], 8 | "offers": ["toothrotErrors"], 9 | "requires": ["resources"], 10 | "modules": ["vrep"], 11 | "resources": { 12 | "toothrotErrors": "errors.json" 13 | }, 14 | "file": "./toothrotErrors.com.js" 15 | } -------------------------------------------------------------------------------- /src/scriptExporter.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptExporter", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["node", "cli", "browser", "any"], 8 | "offers": ["scriptExporter"], 9 | "modules": ["vrep"], 10 | "resources": { 11 | "scriptTemplate": "../misc/templates/script.js", 12 | "scriptsTemplate": "../misc/templates/scripts.js" 13 | }, 14 | "file": "./scriptExporter.com.js" 15 | } -------------------------------------------------------------------------------- /misc/templates/toothrotResources.js: -------------------------------------------------------------------------------- 1 | /* global TOOTHROT */ 2 | 3 | (function () { 4 | 5 | var toothrotResources = "{{$resources}}"; 6 | 7 | // @ts-ignore 8 | TOOTHROT.decorate("getResource", function (fn) { 9 | return function (name) { 10 | 11 | var result = fn(name); 12 | 13 | if (result) { 14 | return result; 15 | } 16 | 17 | return name === "toothrotResources" ? toothrotResources : null; 18 | }; 19 | }); 20 | }()); 21 | -------------------------------------------------------------------------------- /src/runtime/browser-ui/options.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uiOptions", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["uiOptions"], 9 | "file": "./options.com.js", 10 | "channels": { 11 | "exposes": { 12 | "uiOptions": ["add", "addMany", "createWrapper", "createContainer"] 13 | }, 14 | "decorates": { 15 | "ui": ["insertNodeControls", "nodeHasControls"] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // 4 | // # Toothrot CLI Application Bootstrapper 5 | // 6 | // This script bootstraps a `multiversum` app to provide a CLI for toothrot. 7 | // 8 | 9 | /* global process */ 10 | 11 | var createApp = require("../index").createApp; 12 | var parseArgs = require("./args").parse; 13 | 14 | var args = process.argv; 15 | var flags = parseArgs(args).flags; 16 | 17 | var app = createApp({ 18 | patterns: ["**/*.com.json"], 19 | environments: ["cli", "node", "any"], 20 | debug: flags.debug === true, 21 | onError: console.error.bind(console) 22 | }); 23 | 24 | app.init(); 25 | -------------------------------------------------------------------------------- /src/runtime/browser-ui/screens.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uiScreens", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "offers": ["uiScreens"], 9 | "requires": [ 10 | "env", 11 | "vars", 12 | "story", 13 | "focus", 14 | "system", 15 | "scripts", 16 | "storage", 17 | "settings", 18 | "resources", 19 | "uiConfirmations" 20 | ], 21 | "modules": ["vrep", "transform-js"], 22 | "file": "./screens.com.js" 23 | } -------------------------------------------------------------------------------- /resources/resources/templates/confirm.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {message} 4 |
5 |
6 | Yes 13 | No 20 |
21 |
-------------------------------------------------------------------------------- /bin/args.js: -------------------------------------------------------------------------------- 1 | 2 | function parse(args) { 3 | 4 | var result = { 5 | flags: {}, 6 | args: [], 7 | execPath: args[0], 8 | program: args[1] 9 | }; 10 | 11 | args.forEach(function (arg, i) { 12 | 13 | if (i < 2) { 14 | return; 15 | } 16 | 17 | if (isFlag(arg)) { 18 | result.flags[arg.split("--").pop()] = true; 19 | } 20 | else { 21 | result.args.push(arg); 22 | } 23 | }); 24 | 25 | return result; 26 | } 27 | 28 | function isFlag(arg) { 29 | return (/^\-\-/).test(arg); 30 | } 31 | 32 | module.exports = { 33 | parse: parse 34 | }; 35 | -------------------------------------------------------------------------------- /src/runtime/browser-ui/ui.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["run"], 7 | "environments": ["browser"], 8 | "requires": [ 9 | "nodes", 10 | "uiScreens", 11 | "highlighter", 12 | "env", 13 | "vars", 14 | "story", 15 | "system", 16 | "settings", 17 | "resources", 18 | "focus", 19 | "interpreter", 20 | "uiConfirmations", 21 | "uiScrolling", 22 | "uiNotifications", 23 | "uiRevealEffect", 24 | "domUtils" 25 | ], 26 | "modules": ["vrep"], 27 | "file": "./ui.com.js" 28 | } -------------------------------------------------------------------------------- /src/builder.com.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "builder", 3 | "version": "2.0.0", 4 | "application": "toothrot", 5 | "applicationVersion": "2.x", 6 | "applicationSteps": ["build"], 7 | "environments": ["cli", "node"], 8 | "offers": ["builder"], 9 | "requires": ["logger", "packer", "toothrotGatherer", "fileSystem", "scriptExporter"], 10 | "modules": ["path", "assert", "semver", "minimatch", "vrep"], 11 | "resources": { 12 | "toothrotPackageInfo": "../package.json", 13 | "toothrotResourcesTemplate": "../misc/templates/resources.js", 14 | "toothrotComponentTemplate": "../misc/templates/component.js", 15 | "toothrotResourceFileTemplate": "../misc/templates/toothrotResources.js" 16 | }, 17 | "file": "./builder.com.js" 18 | } -------------------------------------------------------------------------------- /src/runtime/resources.com.js: -------------------------------------------------------------------------------- 1 | 2 | function decodeResources(resources) { 3 | return JSON.parse(decodeURIComponent(atob(resources))); 4 | } 5 | 6 | function create(context) { 7 | 8 | // @ts-ignore 9 | var resources = decodeResources(context.channel("getResource")("toothrotResources")); 10 | 11 | var api = context.createInterface("resources", { 12 | get: getResource, 13 | has: hasResource 14 | }); 15 | 16 | context.connectInterface(api); 17 | 18 | function destroy() { 19 | context.disconnectInterface(api); 20 | } 21 | 22 | function getResource(name) { 23 | return resources[name]; 24 | } 25 | 26 | function hasResource(name) { 27 | return name in resources; 28 | } 29 | 30 | return { 31 | destroy: destroy 32 | }; 33 | } 34 | 35 | module.exports = { 36 | create: create 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/browser/domUtils.com.js: -------------------------------------------------------------------------------- 1 | 2 | function getClickableParent(node) { 3 | 4 | var ELEMENT = 1; 5 | var first = node; 6 | 7 | while (node.parentNode) { 8 | 9 | node = node.parentNode; 10 | 11 | if (node.nodeType === ELEMENT && node.getAttribute("data-type")) { 12 | return node; 13 | } 14 | } 15 | 16 | return first; 17 | } 18 | 19 | function create(context) { 20 | 21 | var api = context.createInterface("domUtils", { 22 | getClickableParent: getClickableParent 23 | }); 24 | 25 | function init() { 26 | context.connectInterface(api); 27 | } 28 | 29 | function destroy() { 30 | context.disconnectInterface(api); 31 | } 32 | 33 | return { 34 | init: init, 35 | destroy: destroy 36 | }; 37 | } 38 | 39 | module.exports = { 40 | create: create 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright 2015 - 2018 The toothrot Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 6 | and associated documentation files (the “Software”), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 11 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 12 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 13 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /bin/debug.com.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | function create(context) { 4 | 5 | context.decorate(function (fn, info) { 6 | 7 | var channel = info.channel; 8 | 9 | return function () { 10 | logMessage("Calling channel `" + channel + "` with:\n\n", arguments); 11 | return fn.apply(null, arguments); 12 | }; 13 | }); 14 | 15 | context.decorate("app/publish", function (fn) { 16 | return function (messageName, data) { 17 | logMessage("Publishing message `" + messageName + "` with:\n\n", data || ""); 18 | return fn.apply(null, arguments); 19 | }; 20 | }); 21 | 22 | } 23 | 24 | function logMessage(message, data) { 25 | console.log(""); 26 | console.log("------------------"); 27 | console.log(""); 28 | console.log("[" + new Date().toTimeString() + "] " + message, data); 29 | } 30 | 31 | module.exports = { 32 | create: create 33 | }; 34 | -------------------------------------------------------------------------------- /misc/templates/script.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * {{$description}} 4 | * 5 | * File: {{$file}} 6 | * Line: {{$line}} 7 | * 8 | * @param {object} engine The engine runtime (a multiversum app) 9 | * @param {object} story The story object 10 | * @param {object} _ The function container 11 | * @param {object} $ The variable container 12 | * @param {function} write The `write` function for writing text to the output stream 13 | * @param {function} ln The `ln` function 14 | * @param {string} __file Name of the file that contains the script 15 | * @param {number} __line The line of the script in the story file 16 | * @returns {undefined} Return values are discarded 17 | */ 18 | function {{$functionName}}(engine, story, _, $, write, ln, __file, __line) { 19 | {{$functionBody}} 20 | } 21 | 22 | {{$functionName}}.file = "{{$file}}"; 23 | {{$functionName}}.line = {{$line}}; 24 | 25 | scripts["{{$functionName}}"] = {{$functionName}}; 26 | -------------------------------------------------------------------------------- /misc/templates/resources.js: -------------------------------------------------------------------------------- 1 | /* global TOOTHROT */ 2 | 3 | (function (ids, files) { 4 | 5 | // @ts-ignore 6 | TOOTHROT.decorate("getResource", function (fn) { 7 | return function (name) { 8 | 9 | var result = fn(name); 10 | 11 | if (result) { 12 | return result; 13 | } 14 | 15 | return ids[name] ? files[ids[name]] : null; 16 | }; 17 | }); 18 | 19 | }('{{$resourceIds}}', '{{$resourceFiles}}')); 20 | 21 | (function (mods) { 22 | 23 | // @ts-ignore 24 | TOOTHROT.decorate("getModule", function (fn) { 25 | return function (name) { 26 | 27 | var result = fn(name); 28 | 29 | return result ? result : mods[name]; 30 | }; 31 | }); 32 | 33 | }('{{$mods}}')); 34 | 35 | (function (components) { 36 | 37 | Object.keys(components).forEach(function (key) { 38 | // @ts-ignore 39 | TOOTHROT.addComponent(components[key]); 40 | }); 41 | 42 | }('{{$components}}')); 43 | -------------------------------------------------------------------------------- /resources/files/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Toothrot Engine 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/utils/browser/positioning.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var api = context.createInterface("uiPositioning", { 5 | getScrollX: getScrollX, 6 | getScrollY: getScrollY, 7 | getAbsoluteRect: getAbsoluteRect 8 | }); 9 | 10 | function init() { 11 | context.connectInterface(api); 12 | } 13 | 14 | function destroy() { 15 | context.disconnectInterface(api); 16 | } 17 | 18 | function getScrollX() { 19 | // @ts-ignore 20 | return (window.pageXOffset || document.scrollLeft || 0) - (document.clientLeft || 0); 21 | } 22 | 23 | function getScrollY() { 24 | // @ts-ignore 25 | return (window.pageYOffset || document.scrollTop || 0) - (document.clientTop || 0); 26 | } 27 | 28 | function getAbsoluteRect(element) { 29 | 30 | var rect = element.getBoundingClientRect(); 31 | 32 | return { 33 | left: rect.left + api.getScrollX(), 34 | top: rect.top + api.getScrollY(), 35 | width: rect.width, 36 | height: rect.height 37 | }; 38 | } 39 | 40 | return { 41 | init: init, 42 | destroy: destroy 43 | }; 44 | } 45 | 46 | module.exports = { 47 | create: create 48 | }; 49 | -------------------------------------------------------------------------------- /bin/commands/init.com.js: -------------------------------------------------------------------------------- 1 | /* global process */ 2 | 3 | var fs = require("fs"); 4 | 5 | function create(context) { 6 | 7 | var initializer; 8 | 9 | function init() { 10 | initializer = context.getInterface("initializer", ["init"]); 11 | context.decorate("cli/getCommands", decorate); 12 | } 13 | 14 | function destroy() { 15 | context.removeDecorator("cli/getCommands", decorate); 16 | initializer = null; 17 | } 18 | 19 | function decorate(fn) { 20 | return function () { 21 | 22 | var commands = fn(); 23 | 24 | commands.init = { 25 | run: runInit, 26 | brief: "initializes a new project", 27 | usage: "[]", 28 | description: "Initializes a new project in . " + 29 | "If no is given, the current working directory is used." 30 | }; 31 | 32 | return commands; 33 | }; 34 | } 35 | 36 | function runInit(args) { 37 | 38 | var path = args.args[1]; 39 | 40 | initializer.init(fs, path || process.cwd()); 41 | } 42 | 43 | return { 44 | init: init, 45 | destroy: destroy 46 | }; 47 | } 48 | 49 | module.exports = { 50 | create: create 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toothrot", 3 | "version": "2.0.0", 4 | "description": "Interactive fiction engine based on web technologies.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "keywords": [ 10 | "interactive fiction", 11 | "text adventures", 12 | "choose your own adventure" 13 | ], 14 | "scripts": { 15 | "test": "mocha tests/", 16 | "build-browser-runtime": "node build.js > build/deps-raw.js && browserify build/deps-raw.js > build/toothrot.js && rm build/deps-raw.js" 17 | }, 18 | "dependencies": { 19 | "clone": "^1.0.2", 20 | "colors": "^1.1.2", 21 | "datauri": "^1.1.0", 22 | "deepmerge": "^0.2.10", 23 | "dependency-graph": "^0.7.1", 24 | "howler": "^2.0.14", 25 | "marked": "^0.3.5", 26 | "multiversum": "^0.3.1", 27 | "png-cartridge": "^1.0.1", 28 | "scrollbarwidth": "^0.1.1", 29 | "transform-js": "^1.1.0-final.1512141547", 30 | "vrep": "^2.0.0" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/toothrot-if/toothrot.git" 35 | }, 36 | "author": "J. Steinbeck (https://steinbeck.it)", 37 | "license": "BSD-3-Clause", 38 | "bugs": { 39 | "url": "https://github.com/toothrot-if/toothrot/issues" 40 | }, 41 | "homepage": "https://github.com/toothrot-if/toothrot#readme", 42 | "bin": { 43 | "toothrot": "./bin/cli.js" 44 | }, 45 | "devDependencies": {} 46 | } 47 | -------------------------------------------------------------------------------- /bin/commands/build-desktop.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var builder; 5 | 6 | function init() { 7 | builder = context.getInterface("builder", ["build"]); 8 | context.decorate("cli/getCommands", decorate); 9 | } 10 | 11 | function destroy() { 12 | context.removeDecorator("cli/getCommands", decorate); 13 | builder = null; 14 | } 15 | 16 | function decorate(fn) { 17 | return function () { 18 | 19 | var commands = fn(); 20 | 21 | commands["build-desktop"] = { 22 | run: build, 23 | brief: "builds project for desktop", 24 | usage: "[] []", 25 | description: "Builds the project in and puts the result into ." + 26 | " Also builds desktop apps and puts them in ." + 27 | " If no paths are given, the current working directory is used instead." 28 | }; 29 | 30 | return commands; 31 | }; 32 | } 33 | 34 | function build(args) { 35 | 36 | var path = args.args[1]; 37 | var outputDir = args.args[2]; 38 | 39 | builder.build(path, outputDir, true); 40 | } 41 | 42 | return { 43 | init: init, 44 | destroy: destroy 45 | }; 46 | } 47 | 48 | module.exports = { 49 | create: create 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils/toothrotErrors.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var errors, format; 5 | 6 | var api = context.createInterface("toothrotErrors", { 7 | createError: createError 8 | }); 9 | 10 | function init() { 11 | 12 | var getResource = context.channel("getResource"); 13 | 14 | format = context.channel("getModule").call("vrep").format; 15 | errors = JSON.parse(getResource("toothrotErrors")); 16 | 17 | context.connectInterface(api); 18 | } 19 | 20 | function destroy() { 21 | 22 | errors = null; 23 | format = null; 24 | 25 | context.disconnectInterface(api); 26 | } 27 | 28 | function createError(error) { 29 | 30 | var id = error ? error.id : "[none given]"; 31 | var message = format(errors[id] || "", error || {}); 32 | var toothrotMessage = "\n # Toothrot Error: " + id + "\n\n -> " + message + "\n"; 33 | 34 | if (!(id in errors)) { 35 | throw new Error("Unknown error ID: " + id); 36 | } 37 | 38 | return { 39 | message: message, 40 | isToothrotError: true, 41 | toothrotMessage: toothrotMessage, 42 | id: id, 43 | data: error 44 | }; 45 | } 46 | 47 | return { 48 | init: init, 49 | destroy: destroy 50 | }; 51 | } 52 | 53 | module.exports = { 54 | create: create 55 | }; 56 | -------------------------------------------------------------------------------- /misc/templates/runtime.js: -------------------------------------------------------------------------------- 1 | 2 | /* ---------------------------------------------------------------------------------------------- 3 | 4 | Toothrot Engine Runtime (v'{{$version}}') 5 | 6 | Build Time: '{{$buildTime}}' 7 | 8 | ---------------------------------------------------------------------------------------------- */ 9 | 10 | (function () { 11 | 12 | var app; 13 | 14 | var modules = { 15 | DependencyGraph: require("dependency-graph").DepGraph 16 | }; 17 | 18 | var version = "'{{$version}}'"; 19 | var createApp = require("multiversum/app").create; 20 | var createHost = require("multiversum/host").create; 21 | 22 | var host = createHost(); 23 | 24 | host.connect("getEngineVersion", function () { 25 | return version; 26 | }); 27 | 28 | host.connect("getModule", function (name) { 29 | return modules[name]; 30 | }); 31 | 32 | host.connect("getResource", function () { 33 | return null; 34 | }); 35 | 36 | host.connect("getNodeScript", function () { 37 | return null; 38 | }); 39 | 40 | host.connect("getGlobalScript", function () { 41 | return null; 42 | }); 43 | 44 | host.connect("getSectionScript", function () { 45 | return null; 46 | }); 47 | 48 | app = createApp(host); 49 | 50 | app.on("error", console.error.bind(console)); 51 | app.on("app/error", console.error.bind(console)); 52 | 53 | // @ts-ignore 54 | window.TOOTHROT = app; 55 | 56 | }()); 57 | -------------------------------------------------------------------------------- /resources/resources/screens/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 44 |
-------------------------------------------------------------------------------- /resources/resources/screens/pause.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 45 |
-------------------------------------------------------------------------------- /bin/commands/build.com.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require("fs"); 3 | 4 | function create(context) { 5 | 6 | var builder, logger; 7 | 8 | function init() { 9 | logger = context.getInterface("logger", ["success", "error"]); 10 | builder = context.getInterface("builder", ["build"]); 11 | context.decorate("cli/getCommands", decorate); 12 | } 13 | 14 | function destroy() { 15 | context.removeDecorator("cli/getCommands", decorate); 16 | builder = null; 17 | logger = null; 18 | } 19 | 20 | function decorate(fn) { 21 | return function () { 22 | 23 | var commands = fn(); 24 | 25 | commands.build = { 26 | run: build, 27 | brief: "builds project for browser", 28 | usage: "[] []", 29 | description: "Builds the project in and puts the result into ." + 30 | " If no paths are given, the current working directory is used instead." 31 | }; 32 | 33 | return commands; 34 | }; 35 | } 36 | 37 | function build(args) { 38 | 39 | var path = args.args[1]; 40 | var outputDir = args.args[2]; 41 | 42 | builder.build(fs, path, fs, outputDir, function (errors) { 43 | if (errors) { 44 | logger.error("Project cannot be build because it contains errors."); 45 | } 46 | else { 47 | logger.success("Project build successfully! :)"); 48 | } 49 | }); 50 | } 51 | 52 | return { 53 | init: init, 54 | destroy: destroy 55 | }; 56 | } 57 | 58 | module.exports = { 59 | create: create 60 | }; 61 | -------------------------------------------------------------------------------- /src/runtime/env.com.js: -------------------------------------------------------------------------------- 1 | // 2 | // # Script environment component 3 | // 4 | // The environment for scripts. It's available in scripts as: _ 5 | // 6 | 7 | function create(context) { 8 | 9 | var env; 10 | 11 | var api = context.createInterface("env", { 12 | get: get, 13 | has: has, 14 | set: set, 15 | getAll: getAll, 16 | createEnvObject: createEnvObject 17 | }); 18 | 19 | function init() { 20 | context.connectInterface(api); 21 | env = api.createEnvObject(); 22 | } 23 | 24 | function destroy() { 25 | env = null; 26 | context.disconnectInterface(api); 27 | } 28 | 29 | function createEnvObject() { 30 | return { 31 | oneOf: function () { 32 | return arguments[Math.floor(Math.random() * arguments.length)]; 33 | }, 34 | save: function (savegameId, then) { 35 | setTimeout(function () { 36 | context.channel("interpreter/save").call(savegameId, then || function () {}); 37 | }, 20); 38 | }, 39 | load: function (savegameId, then) { 40 | setTimeout(function () { 41 | context.channel("interpreter/load").call(savegameId, then || function () {}); 42 | }, 20); 43 | } 44 | }; 45 | } 46 | 47 | function set(key, value) { 48 | env[key] = value; 49 | } 50 | 51 | function get(key) { 52 | return env[key]; 53 | } 54 | 55 | function has(key) { 56 | return (key in env); 57 | } 58 | 59 | function getAll() { 60 | return env; 61 | } 62 | 63 | return { 64 | init: init, 65 | destroy: destroy 66 | }; 67 | } 68 | 69 | module.exports = { 70 | create: create 71 | }; 72 | -------------------------------------------------------------------------------- /resources/resources/screens/settings.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | 8 |
9 |
10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
Save
32 |
Back
39 |
40 |
41 |
-------------------------------------------------------------------------------- /src/runtime/vars.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var vars; 5 | 6 | var api = context.createInterface("vars", { 7 | get: get, 8 | getAll: getAll, 9 | has: has, 10 | remove: remove, 11 | clear: clear, 12 | set: set, 13 | onResume: onResume 14 | }); 15 | 16 | function init() { 17 | 18 | vars = {}; 19 | context.on("resume_game", api.onResume); 20 | 21 | context.connectInterface(api); 22 | } 23 | 24 | function destroy() { 25 | context.disconnectInterface(api); 26 | vars = null; 27 | } 28 | 29 | function onResume(data) { 30 | 31 | api.clear(); 32 | 33 | Object.keys(data.vars).forEach(function (key) { 34 | vars[key] = data.vars[key]; 35 | }); 36 | 37 | context.publish("vars_resume"); 38 | } 39 | 40 | function get(key) { 41 | return vars[key]; 42 | } 43 | 44 | function remove(key) { 45 | delete vars[key]; 46 | } 47 | 48 | function getAll() { 49 | return vars; 50 | } 51 | 52 | function set(key, value) { 53 | context.publish("before_set_var", key); 54 | vars[key] = value; 55 | context.publish("set_var", key); 56 | } 57 | 58 | function has(key) { 59 | return (key in vars); 60 | } 61 | 62 | function clear() { 63 | Object.keys(vars).forEach(function (key) { 64 | delete vars[key]; 65 | }); 66 | } 67 | 68 | return { 69 | init: init, 70 | destroy: destroy 71 | }; 72 | } 73 | 74 | module.exports = { 75 | name: "vars", 76 | version: "2.0.0", 77 | application: "toothrot", 78 | applicationVersion: "2.x", 79 | applicationSteps: ["run"], 80 | environments: ["any"], 81 | create: create 82 | }; 83 | -------------------------------------------------------------------------------- /src/utils/appCacheManifest.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var path, fsHelper, logger; 5 | 6 | var api = context.createInterface("appCacheManifest", { 7 | create: createManifest 8 | }); 9 | 10 | function init() { 11 | 12 | path = context.channel("getModule")("path"); 13 | fsHelper = context.getInterface("fileSystem", ["readDirRecursive"]); 14 | logger = context.getInterface("logger", ["success"]); 15 | 16 | context.connectInterface(api); 17 | context.subscribe("builder/build.after", onBuild); 18 | } 19 | 20 | function destroy() { 21 | 22 | fsHelper = null; 23 | 24 | context.disconnectInterface(api); 25 | context.unsubscribe("builder/build.after", onBuild); 26 | } 27 | 28 | function onBuild(data) { 29 | createManifest(data.outputFs, data.browserDir); 30 | } 31 | 32 | function createManifest(fs, dir) { 33 | 34 | var cacheFile = "" + 35 | "CACHE MANIFEST\n" + 36 | "# Timestamp: " + Date.now() + "\n" + 37 | "# Automatically created by Toothrot Engine\n" + 38 | "\n" + 39 | "CACHE:\n"; 40 | 41 | var cacheFilePath = path.join(dir, "cache.manifest"); 42 | var files = fsHelper.readDirRecursive(fs, dir); 43 | 44 | files.forEach(function (file) { 45 | cacheFile += normalizePath(file) + "\n"; 46 | }); 47 | 48 | fs.writeFileSync(cacheFilePath, cacheFile); 49 | 50 | logger.success("Created appcache file at: " + cacheFilePath); 51 | 52 | function normalizePath(path) { 53 | return path.replace("\\", "/"); 54 | } 55 | } 56 | 57 | return { 58 | init: init, 59 | destroy: destroy 60 | }; 61 | } 62 | 63 | module.exports = { 64 | create: create 65 | }; 66 | -------------------------------------------------------------------------------- /src/utils/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "NO_TITLE": "No story title specified!", 4 | "NO_START_NODE": "Required node 'start' is missing.", 5 | "SETTINGS_JSON_ERROR": "The settings section contains mal-formed JSON.", 6 | "HIERARCHY_JSON_ERROR": "The hierarchy section contains mal-formed JSON.", 7 | "CIRCULAR_HIERARCHY": "The hierarchy is circular. Tag '{tag}' references itself.", 8 | "UNKNOWN_NEXT_NODE": "Unknown next node '{next}' for node '{nodeId}' (<{nodeFile}>@{nodeLine}).", 9 | "UNKNOWN_AUTONEXT_TARGET": "Unknown autonextTarget '{target}' for node '{nodeId}' (<{nodeFile}>@{nodeLine}).", 10 | "CONFLICT_NEXT_RETURN": "Conflict: Node '{nodeId}' (<{nodeFile}>@{nodeLine}) has both a next node and a return to the last node.", 11 | "NO_AUTONEXT_TARGET": "Node '{nodeId}' (<{nodeFile}>@{nodeLine}) has an autonext property but no autonextTarget, next node or return is specified.", 12 | "OPTION_WITHOUT_TARGET_OR_VALUE": "Option must have at least one of 'target' or 'value' in node '{nodeId}' (<{nodeFile}>@{nodeLine}). <{optionFile}>@{optionLine}", 13 | "UNKNOWN_OPTION_TARGET": "Unknown node '{target}' referenced in option '{label}' in node '{nodeId}' (<{nodeFile}>@{nodeLine}). <{optionFile}>@{optionLine}", 14 | "NO_LINK_TARGET": "No link target specified for link '{label}' in node '{nodeId}' (<{file}>@{nodeLine}). <{file}>@{linkLine}", 15 | "UNKNOWN_LINK_TARGET": "No such link target '{target}' for link '{label}' in node '{nodeId}' (<{file}>@{nodeLine}). <{file}>@{linkLine}", 16 | "MULTIPLE_NEXT_NODES": "Node '{nodeId}' (<{file}>@{nodeLine}) cannot have more than one next node. <{file}>@{lineOffset}", 17 | "MALFORMED_OPTION": "Malformed option in node '{nodeId}' (<{file}>@{nodeLine}). <{file}>@{lineOffset}", 18 | "INVALID_JSON_IN_SECTION_PROPERTY": "Cannot parse property '{property}' in section '{section}' (<{sectionFile}>@{sectionLine}): {errorMessage}", 19 | "INVALID_JSON_IN_NODE_PROPERTY": "Cannot parse property '{property}' in node '{nodeId}' (<{nodeFile}>@{nodeLine}): {errorMessage}" 20 | } -------------------------------------------------------------------------------- /misc/templates/scripts.js: -------------------------------------------------------------------------------- 1 | // 2 | // {{$description}} 3 | // 4 | // Engine version: {{$engineVersion}} 5 | // Build time: {{$buildTime}} 6 | // 7 | 8 | /* global TOOTHROT */ 9 | 10 | (function () { 11 | 12 | var scripts = {}; 13 | 14 | /* eslint-disable camelcase */ 15 | /* eslint-disable no-unused-vars */ 16 | 17 | {{$functions}} 18 | 19 | /* eslint-enable camelcase */ 20 | /* eslint-enable no-unused-vars */ 21 | 22 | // @ts-ignore 23 | TOOTHROT.decorate("getGlobalScript", function (fn) { 24 | return function (name) { 25 | 26 | var scriptName; 27 | var result = fn(name); 28 | 29 | if (result) { 30 | return result; 31 | } 32 | 33 | scriptName = "global__slot_" + name; 34 | 35 | return scriptName in scripts ? scripts[scriptName] : null; 36 | }; 37 | }); 38 | 39 | // @ts-ignore 40 | TOOTHROT.decorate("getSectionScript", function (fn) { 41 | return function (sectionName, slotName) { 42 | 43 | var scriptName; 44 | var result = fn(sectionName, slotName); 45 | 46 | if (result) { 47 | return result; 48 | } 49 | 50 | scriptName = "section_" + sectionName + "__slot_" + slotName; 51 | 52 | return scriptName in scripts ? scripts[scriptName] : null; 53 | }; 54 | }); 55 | 56 | // @ts-ignore 57 | TOOTHROT.decorate("getNodeScript", function (fn) { 58 | return function (nodeName, slotName) { 59 | 60 | var scriptName; 61 | var result = fn(nodeName, slotName); 62 | 63 | if (result) { 64 | return result; 65 | } 66 | 67 | scriptName = "node_" + nodeName + "__slot_" + slotName; 68 | 69 | return scriptName in scripts ? scripts[scriptName] : null; 70 | }; 71 | }); 72 | 73 | }()); 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toothrot Engine 2 | 3 | DEPRECATION NOTICE: This project is no longer actively maintained. 4 | 5 | An engine for creating text-based games. 6 | 7 | Toothrot Engine allows you to create interactive fiction, parser-less text adventures or other 8 | text-based games. The games are written in an eye-friendly text-based format (similar to Markdown) 9 | and allow writing your game logic in JavaScript. 10 | 11 | To develop games with this engine, basic knowledge of JavaScript and HTML is recommended, 12 | though not required for simple choice-based or hypertext games. 13 | 14 | **Please note:** For most people [Toothrot IDE](https://github.com/toothrot-if/toothrot-ide/) 15 | is the better choice for developing Toothrot games. 16 | 17 | ## Features 18 | 19 | * Markdown-like format for writing games 20 | * Different modes of interaction: 21 | * Regular links (like known from Twine) 22 | * Option menus (like in ChoiceScript or in visual novels) 23 | * Going to the next node by clicking or pushing a button (like in visual novels) 24 | * Customizable screen system written in regular HTML and CSS with default screens: 25 | * Main screen 26 | * Pause screen 27 | * Savegame screen 28 | * Settings screen 29 | * Savegame system with slots, auto-save and quick save/load 30 | * Text nodes can also be things in the simple world model, e.g. rooms, items or persons 31 | * Text nodes can be tagged and put into a hierarchy 32 | * Text nodes can contain other text nodes 33 | * Support for mobile devices (and "add to homescreen") 34 | * Exports games for browsers (works without a web server) or as Windows/Mac/Linux desktop apps 35 | * Audio support with separate channels for sounds, ambience and music 36 | * Games playable using the keyboard 37 | * Games playable with screen readers (experimental) 38 | * Extensible JavaScript API 39 | * Speed-adjustable reveal effect for text (like in visual novels) 40 | * Browser builds support application cache (offline mode) out of the box 41 | 42 | ## Documentation 43 | 44 | The documentation resides in its own repository and can be found 45 | [here](https://github.com/toothrot-if/toothrot-docs/). 46 | -------------------------------------------------------------------------------- /src/utils/browser/confirmations.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var api = context.createInterface("uiConfirmations", { 5 | create: createConfirm 6 | }); 7 | 8 | function init() { 9 | context.connectInterface(api); 10 | } 11 | 12 | function destroy() { 13 | context.disconnectInterface(api); 14 | } 15 | 16 | function createConfirm() { 17 | 18 | function confirm(text, then) { 19 | 20 | var template = context.channel("resources/get").call("templates").confirm; 21 | var focus = context.getInterface("focus", ["getMode", "setMode"]); 22 | var boxContainer = document.createElement("div"); 23 | var oldFocus = focus.getMode(); 24 | 25 | focus.setMode("messagebox"); 26 | 27 | boxContainer.setAttribute("class", "message-box-container"); 28 | 29 | boxContainer.innerHTML = template.replace("{message}", text); 30 | 31 | boxContainer.addEventListener("click", onClick); 32 | document.body.appendChild(boxContainer); 33 | 34 | boxContainer.focus(); 35 | 36 | function onClick(event) { 37 | 38 | var type = event.target.getAttribute("data-type"); 39 | var value = event.target.getAttribute("data-value"); 40 | 41 | if (type === "messagebox-button") { 42 | 43 | event.stopPropagation(); 44 | event.preventDefault(); 45 | 46 | focus.setMode(oldFocus); 47 | boxContainer.parentNode.removeChild(boxContainer); 48 | boxContainer.removeEventListener("click", onClick); 49 | 50 | then(value === "yes" ? true : false); 51 | } 52 | } 53 | } 54 | 55 | return confirm; 56 | } 57 | 58 | return { 59 | init: init, 60 | destroy: destroy 61 | }; 62 | } 63 | 64 | module.exports = { 65 | create: create 66 | }; 67 | -------------------------------------------------------------------------------- /bin/commands/pack.com.js: -------------------------------------------------------------------------------- 1 | /* global process */ 2 | 3 | var fs = require("fs"); 4 | 5 | function create(context) { 6 | 7 | var packer, logger; 8 | 9 | function init() { 10 | logger = context.getInterface("logger", ["error"]); 11 | packer = context.getInterface("packer", ["pack"]); 12 | context.decorate("cli/getCommands", decorate); 13 | } 14 | 15 | function destroy() { 16 | context.removeDecorator("cli/getCommands", decorate); 17 | packer = null; 18 | logger = null; 19 | } 20 | 21 | function decorate(fn) { 22 | return function () { 23 | 24 | var commands = fn(); 25 | 26 | commands.pack = { 27 | run: pack, 28 | brief: "packs the project resources into a resource file", 29 | usage: "[]", 30 | description: "Packs raw resources of the project in and outputs " + 31 | "a JSON object containing the packed resources. If no is given, " + 32 | "the current working directory is used." 33 | }; 34 | 35 | return commands; 36 | }; 37 | } 38 | 39 | function pack(args) { 40 | 41 | var path = args.args[1] || process.cwd(); 42 | 43 | packer.pack(fs, path, function (errors, storyPack) { 44 | 45 | if (errors) { 46 | 47 | if (Array.isArray(errors)) { 48 | errors.forEach(function (error) { 49 | logger.error(error.toothrotMessage || error.message); 50 | }); 51 | } 52 | else if (errors) { 53 | logger.error(errors); 54 | } 55 | 56 | logger.error("Project resources could not be packed because of errors."); 57 | } 58 | else { 59 | console.log(storyPack); // eslint-disable-line no-console 60 | } 61 | }); 62 | } 63 | 64 | return { 65 | init: init, 66 | destroy: destroy 67 | }; 68 | } 69 | 70 | module.exports = { 71 | create: create 72 | }; 73 | -------------------------------------------------------------------------------- /bin/commands/parse.com.js: -------------------------------------------------------------------------------- 1 | /* global process */ 2 | 3 | var fs = require("fs"); 4 | var joinPath = require("path").join; 5 | 6 | function create(context) { 7 | 8 | var parser, logger, reader; 9 | 10 | function init() { 11 | parser = context.getInterface("parser", ["parse"]); 12 | logger = context.getInterface("logger", ["error"]); 13 | reader = context.getInterface("storyFileReader", ["read"]); 14 | context.decorate("cli/getCommands", decorate); 15 | } 16 | 17 | function destroy() { 18 | context.removeDecorator("cli/getCommands", decorate); 19 | parser = null; 20 | logger = null; 21 | } 22 | 23 | function decorate(fn) { 24 | return function () { 25 | 26 | var commands = fn(); 27 | 28 | commands.parse = { 29 | run: parse, 30 | brief: "parses a story file", 31 | usage: "[]", 32 | description: "Parses the story file at . " + 33 | "If no is given, the current working directory is used." 34 | }; 35 | 36 | return commands; 37 | }; 38 | } 39 | 40 | function parse(args) { 41 | 42 | var storyFiles; 43 | var path = joinPath(args.args[1] || process.cwd(), "/resources/"); 44 | 45 | if (!path || typeof path !== "string") { 46 | logger.error("No path specified for `parse` command!"); 47 | return; 48 | } 49 | 50 | storyFiles = reader.read(fs, path); 51 | 52 | parser.parse(storyFiles, function (errors, result) { 53 | if (errors) { 54 | reportErrors(errors); 55 | } 56 | else { 57 | console.log(JSON.stringify(result, null, 4)); // eslint-disable-line no-console 58 | } 59 | }); 60 | } 61 | 62 | function reportErrors(errors) { 63 | errors.forEach(function (error) { 64 | logger.error(error.toothrotMessage || error.message); 65 | }); 66 | } 67 | 68 | return { 69 | init: init, 70 | destroy: destroy 71 | }; 72 | } 73 | 74 | module.exports = { 75 | create: create 76 | }; 77 | -------------------------------------------------------------------------------- /bin/commands/validate.com.js: -------------------------------------------------------------------------------- 1 | /* global process */ 2 | 3 | var fs = require("fs"); 4 | var joinPath = require("path").join; 5 | 6 | function create(context) { 7 | 8 | var logger, parser, reader; 9 | 10 | function init() { 11 | logger = context.getInterface("logger", ["error", "success"]); 12 | parser = context.getInterface("parser", ["parse"]); 13 | reader = context.getInterface("storyFileReader", ["read"]); 14 | context.decorate("cli/getCommands", decorate); 15 | } 16 | 17 | function destroy() { 18 | context.removeDecorator("cli/getCommands", decorate); 19 | logger = null; 20 | } 21 | 22 | function decorate(fn) { 23 | return function () { 24 | 25 | var commands = fn(); 26 | 27 | commands.validate = { 28 | run: validate, 29 | brief: "validates a story file", 30 | usage: "[]", 31 | description: "Initializes a new project in . " + 32 | "If no is given, the current working directory is used." 33 | }; 34 | 35 | return commands; 36 | }; 37 | } 38 | 39 | function validate(args) { 40 | 41 | var path = args.args[1] || process.cwd(); 42 | var storyFiles = reader.read(fs, joinPath(path, "/resources/")); 43 | 44 | if (args.flags.json) { 45 | parser.parse(storyFiles, handleErrorsJson); 46 | } 47 | else { 48 | parser.parse(storyFiles, handleErrors); 49 | } 50 | } 51 | 52 | function handleErrorsJson(errors) { 53 | console.log( // eslint-disable-line no-console 54 | Array.isArray(errors) ? 55 | JSON.stringify(errors, null, 4) : 56 | [] 57 | ); 58 | } 59 | 60 | function handleErrors(errors) { 61 | if (errors) { 62 | if (Array.isArray(errors)) { 63 | errors.forEach(function (error) { 64 | logger.error("\n" + (error.toothrotMessage || error.message)); 65 | }); 66 | } 67 | else { 68 | logger.error(errors); 69 | } 70 | } 71 | else { 72 | logger.success("Story files are valid!"); 73 | } 74 | } 75 | 76 | return { 77 | init: init, 78 | destroy: destroy 79 | }; 80 | } 81 | 82 | module.exports = { 83 | create: create 84 | }; 85 | -------------------------------------------------------------------------------- /src/utils/logger.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var colors; 5 | 6 | var api = context.createInterface("logger", { 7 | log: log, 8 | error: logError, 9 | info: logInfo, 10 | warn: warn, 11 | success: logSuccess 12 | }); 13 | 14 | function init() { 15 | colors = context.channel("getModule").call("colors"); 16 | context.connectInterface(api); 17 | } 18 | 19 | function destroy() { 20 | context.disconnectInterface(api); 21 | } 22 | 23 | function getTimeString() { 24 | // @ts-ignore 25 | return colors.grey("[" + (new Date()).toLocaleTimeString() + "]"); 26 | } 27 | 28 | function log() { 29 | 30 | var args = Array.prototype.slice.call(arguments); 31 | 32 | args.unshift(getTimeString()); 33 | 34 | console.log.apply(console, args); // eslint-disable-line no-console 35 | } 36 | 37 | function warn() { 38 | 39 | var args = Array.prototype.slice.call(arguments).map(function (message) { 40 | // @ts-ignore 41 | return colors.yellow(message); 42 | }); 43 | 44 | args.unshift(getTimeString()); 45 | 46 | console.warn.apply(null, args); 47 | } 48 | 49 | function logError() { 50 | 51 | var args = Array.prototype.slice.call(arguments).map(function (message) { 52 | // @ts-ignore 53 | return colors.red(message); 54 | }); 55 | 56 | args.unshift(getTimeString()); 57 | 58 | console.error.apply(null, args); 59 | } 60 | 61 | function logSuccess() { 62 | 63 | var args = Array.prototype.slice.call(arguments).map(function (message) { 64 | // @ts-ignore 65 | return colors.green(message); 66 | }); 67 | 68 | args.unshift(getTimeString()); 69 | 70 | console.log.apply(null, args); // eslint-disable-line no-console 71 | } 72 | 73 | function logInfo() { 74 | 75 | var args = Array.prototype.slice.call(arguments).map(function (message) { 76 | // @ts-ignore 77 | return colors.blue(message); 78 | }); 79 | 80 | args.unshift(getTimeString()); 81 | 82 | console.log.apply(null, args); // eslint-disable-line no-console 83 | } 84 | 85 | return { 86 | init: init, 87 | destroy: destroy 88 | }; 89 | } 90 | 91 | module.exports = { 92 | create: create 93 | }; 94 | -------------------------------------------------------------------------------- /resources/resources/screens/cartridges.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |

Save

7 |

8 | Save the image below to your device. It contains the current state of your 9 | game, your settings, and what's in your quicksave slot. 10 |

11 | 15 | Click to download current datacard 19 | 20 |
21 |
22 |

Load

23 |
24 | 25 |
26 |

27 | Click to select a datacard image to load 28 |

29 |
30 |
31 |
32 | 33 |
34 | 40 |
41 |
42 | 64 |
-------------------------------------------------------------------------------- /src/storyFileReader.com.js: -------------------------------------------------------------------------------- 1 | 2 | var EXTENSION = /\.tr\.md/; 3 | var MAIN_FILE = "main.tr.md"; 4 | var MAIN_FILE_NAME = /main\.tr\.md/; 5 | 6 | function create(context) { 7 | 8 | var joinPath, fileSystem; 9 | 10 | var api = context.createInterface("storyFileReader", { 11 | read: readStoryFiles, 12 | isStoryFile: isStoryFile, 13 | isMainStoryFile: isMainStoryFile, 14 | fileNameToMarker: fileNameToMarker, 15 | getStoryFileExtensionTest: getStoryFileExtensionTest, 16 | getMainStoryFileTest: getMainStoryFileTest, 17 | getMainStoryFileName: getMainStoryFileName 18 | }); 19 | 20 | function init() { 21 | joinPath = context.channel("getModule")("path").join; 22 | fileSystem = context.getInterface("fileSystem", ["readDirRecursive"]); 23 | context.connectInterface(api); 24 | } 25 | 26 | function destroy() { 27 | context.disconnectInterface(api); 28 | fileSystem = null; 29 | } 30 | 31 | function readStoryFiles(fs, dir) { 32 | 33 | var files = fileSystem.readDirRecursive(fs, dir).filter(function (file) { 34 | 35 | if (api.isMainStoryFile(file)) { 36 | return false; 37 | } 38 | 39 | return api.isStoryFile(file); 40 | }); 41 | 42 | var mainFileName = api.getMainStoryFileName(); 43 | var mainFile = joinPath(dir, mainFileName); 44 | var content = api.fileNameToMarker(mainFileName) + "\n"; 45 | 46 | content += fs.readFileSync(mainFile); 47 | 48 | files.forEach(function (file) { 49 | 50 | var fileContent = fs.readFileSync(joinPath(dir, file)); 51 | 52 | content += api.fileNameToMarker(file) + "\n"; 53 | content += fileContent; 54 | }); 55 | 56 | return content; 57 | } 58 | 59 | function isMainStoryFile(fileName) { 60 | return api.getMainStoryFileTest().test(fileName); 61 | } 62 | 63 | function isStoryFile(fileName) { 64 | return api.getStoryFileExtensionTest().test(fileName); 65 | } 66 | 67 | function getMainStoryFileName() { 68 | return MAIN_FILE; 69 | } 70 | 71 | function fileNameToMarker(fileName) { 72 | return "<<<" + fileName + ">>>"; 73 | } 74 | 75 | function getMainStoryFileTest() { 76 | return MAIN_FILE_NAME; 77 | } 78 | 79 | function getStoryFileExtensionTest() { 80 | return EXTENSION; 81 | } 82 | 83 | return { 84 | init: init, 85 | destroy: destroy 86 | }; 87 | } 88 | 89 | module.exports = { 90 | create: create 91 | }; 92 | -------------------------------------------------------------------------------- /src/utils/scripts.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var story, env, vars, logger, getNodeScript, getGlobalScript, getSectionScript; 5 | 6 | var api = context.createInterface("scripts", { 7 | run: runScript, 8 | write: write, 9 | writeLn: writeLn, 10 | runNodeScript: runNodeScript, 11 | runGlobalScript: runGlobalScript, 12 | runSectionScript: runSectionScript 13 | }); 14 | 15 | function init() { 16 | 17 | getNodeScript = context.channel("getNodeScript"); 18 | getGlobalScript = context.channel("getGlobalScript"); 19 | getSectionScript = context.channel("getSectionScript"); 20 | 21 | env = context.getInterface("env", ["getAll"]); 22 | vars = context.getInterface("vars", ["getAll"]); 23 | story = context.getInterface("story", ["getAll"]); 24 | logger = context.getInterface("logger", ["error"]); 25 | 26 | context.connectInterface(api); 27 | } 28 | 29 | function destroy() { 30 | context.disconnectInterface(api); 31 | } 32 | 33 | function runNodeScript(nodeName, slotName) { 34 | return runScript(getNodeScript(nodeName, slotName)); 35 | } 36 | 37 | function runSectionScript(sectionName, slotName) { 38 | return runScript(getSectionScript(sectionName, slotName)); 39 | } 40 | 41 | function runGlobalScript(slotName) { 42 | return runScript(getGlobalScript(slotName)); 43 | } 44 | 45 | function runScript(script) { 46 | 47 | var result = ""; 48 | 49 | try { 50 | 51 | result = ""; 52 | 53 | script( 54 | context, 55 | story.getAll(), 56 | env.getAll(), 57 | vars.getAll(), 58 | write, 59 | writeLn, 60 | script.line, 61 | script.file 62 | ); 63 | } 64 | catch (error) { 65 | logger.error( 66 | "Cannot execute script (<" + script.file + ">@" + script.line + "):", 67 | error 68 | ); 69 | } 70 | 71 | return result; 72 | 73 | function write(text) { 74 | result += api.write(text); 75 | } 76 | 77 | function writeLn(text) { 78 | result += api.writeLn(text); 79 | } 80 | } 81 | 82 | function write(text) { 83 | return text; 84 | } 85 | 86 | function writeLn(text) { 87 | return "\n
" + text; 88 | } 89 | 90 | return { 91 | init: init, 92 | destroy: destroy 93 | }; 94 | } 95 | 96 | module.exports = { 97 | create: create 98 | }; 99 | -------------------------------------------------------------------------------- /bin/cli.com.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* global process */ 3 | 4 | var parseArgs = require("./args").parse; 5 | 6 | // @ts-ignore 7 | var package = require("../package.json"); 8 | 9 | function create(context) { 10 | 11 | var api = context.createInterface("cli", { 12 | run: run, 13 | getCommands: getCommands 14 | }); 15 | 16 | function init() { 17 | context.connectInterface(api); 18 | context.once("app/ready", api.run); 19 | } 20 | 21 | function destroy() { 22 | context.disconnectInterface(api); 23 | } 24 | 25 | function getCommands() { 26 | return { 27 | help: { 28 | run: showInfo, 29 | brief: "shows help/info for a command", 30 | usage: "[]", 31 | description: "Shows general help when used without a command name. " + 32 | "Shows help for a command if used with a command name." 33 | } 34 | }; 35 | } 36 | 37 | function showInfo(args) { 38 | 39 | var command = args.args[1]; 40 | var commands = api.getCommands(); 41 | 42 | if (command && command in commands) { 43 | console.log(""); 44 | console.log("Command `" + command + "`"); 45 | console.log("--------------------------------"); 46 | console.log(""); 47 | console.log(commands[command].description); 48 | console.log(""); 49 | console.log("Usage: toothrot " + command + " " + commands[command].usage); 50 | console.log(""); 51 | return; 52 | } 53 | 54 | console.log(""); 55 | console.log("Toothrot CLI (v" + package.version + ")"); 56 | console.log("--------------------------------------------"); 57 | console.log(""); 58 | console.log("Run `toothrot help ` to get help about a command."); 59 | console.log(""); 60 | console.log("The following commands are available:"); 61 | console.log(""); 62 | 63 | Object.keys(commands).forEach(function (commandName) { 64 | console.log(" * " + commandName + " -- " + commands[commandName].brief); 65 | }); 66 | 67 | console.log(""); 68 | 69 | } 70 | 71 | function run() { 72 | 73 | var commands = api.getCommands(); 74 | var args = parseArgs(process.argv); 75 | var command = args.args[0]; 76 | 77 | if (!command && args.flags.version) { 78 | console.log(package.version); 79 | return; 80 | } 81 | 82 | command = command && command in commands ? command : "help"; 83 | 84 | commands[command].run(args); 85 | 86 | } 87 | 88 | return { 89 | init: init, 90 | destroy: destroy 91 | }; 92 | } 93 | 94 | module.exports = { 95 | create: create 96 | }; 97 | -------------------------------------------------------------------------------- /src/utils/browser/notifications.com.js: -------------------------------------------------------------------------------- 1 | /* global setTimeout */ 2 | 3 | function create(context) { 4 | 5 | var format, transform; 6 | 7 | var api = context.createInterface("uiNotifications", { 8 | create: createNotification 9 | }); 10 | 11 | function init() { 12 | 13 | var getModule = context.channel("getModule").call; 14 | 15 | format = getModule("vrep").format; 16 | transform = getModule("transform-js").transform; 17 | 18 | context.connectInterface(api); 19 | } 20 | 21 | function destroy() { 22 | context.disconnectInterface(api); 23 | } 24 | 25 | function createNotification(template, fadeDuration) { 26 | 27 | var duration = fadeDuration || 200; 28 | 29 | return function (message, type, timeout) { 30 | 31 | var container = document.createElement("div"); 32 | var hidden = false; 33 | var shown = false; 34 | 35 | container.setAttribute("class", "notification-container"); 36 | 37 | type = type || "default"; 38 | 39 | container.style.opacity = "0"; 40 | container.innerHTML = format(template, {message: message, type: type}); 41 | 42 | document.body.appendChild(container); 43 | 44 | show(); 45 | 46 | setTimeout(hide, timeout || 2000); 47 | 48 | function show() { 49 | 50 | if (shown) { 51 | return; 52 | } 53 | 54 | transform( 55 | 0, 56 | 1, 57 | function (v) { 58 | container.style.opacity = "" + v; 59 | }, 60 | {duration: duration}, 61 | function () { 62 | shown = true; 63 | } 64 | ); 65 | } 66 | 67 | function hide() { 68 | 69 | if (hidden) { 70 | return; 71 | } 72 | 73 | transform( 74 | 1, 75 | 0, 76 | function (v) { 77 | container.style.opacity = "" + v; 78 | }, 79 | {duration: duration}, 80 | function () { 81 | hidden = true; 82 | container.parentNode.removeChild(container); 83 | } 84 | ); 85 | } 86 | 87 | return { 88 | hide: hide 89 | }; 90 | }; 91 | } 92 | 93 | return { 94 | init: init, 95 | destroy: destroy 96 | }; 97 | } 98 | 99 | module.exports = { 100 | create: create 101 | }; 102 | -------------------------------------------------------------------------------- /src/runtime/focus.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var mode, modes, focusOffset; 5 | 6 | var api = context.createInterface("focus", { 7 | getMode: getMode, 8 | setMode: setMode, 9 | hasMode: hasMode, 10 | getElements: getElements, 11 | getElementInFocus: getElementInFocus, 12 | execute: execute, 13 | next: next, 14 | previous: previous, 15 | reset: reset, 16 | count: count 17 | }); 18 | 19 | function init() { 20 | 21 | modes = {}; 22 | 23 | context.connectInterface(api); 24 | } 25 | 26 | function destroy() { 27 | 28 | context.disconnectInterface(api); 29 | 30 | modes = null; 31 | } 32 | 33 | function getElements() { 34 | return document.querySelectorAll("[data-focus-mode='" + mode + "']"); 35 | } 36 | 37 | function setMode(newMode) { 38 | context.publish("before_change_focus_mode", mode); 39 | mode = newMode; 40 | context.publish("change_focus_mode", mode); 41 | } 42 | 43 | function getMode() { 44 | return mode; 45 | } 46 | 47 | function hasMode(name) { 48 | return (name in modes); 49 | } 50 | 51 | function previous() { 52 | 53 | var element; 54 | 55 | if (typeof focusOffset !== "number") { 56 | focusOffset = 0; 57 | } 58 | 59 | focusOffset -= 1; 60 | 61 | if (focusOffset < 0) { 62 | focusOffset = api.count() - 1; 63 | } 64 | 65 | element = api.getElementInFocus(); 66 | 67 | context.publish("focus_previous", element); 68 | context.publish("focus_change", element); 69 | } 70 | 71 | function next() { 72 | 73 | var element; 74 | 75 | if (typeof focusOffset !== "number") { 76 | focusOffset = -1; 77 | } 78 | 79 | focusOffset += 1; 80 | 81 | if (focusOffset > api.count() - 1) { 82 | focusOffset = 0; 83 | } 84 | 85 | element = api.getElementInFocus(); 86 | 87 | context.publish("focus_next", element); 88 | context.publish("focus_change", element); 89 | } 90 | 91 | function execute() { 92 | 93 | if (typeof focusOffset === "number") { 94 | api.getElementInFocus().click(); 95 | api.reset(); 96 | } 97 | else { 98 | // @ts-ignore 99 | document.activeElement.click(); 100 | } 101 | } 102 | 103 | function getElementInFocus() { 104 | return api.getElements()[focusOffset]; 105 | } 106 | 107 | function count() { 108 | return api.getElements().length; 109 | } 110 | 111 | function reset() { 112 | focusOffset = undefined; 113 | } 114 | 115 | return { 116 | init: init, 117 | destroy: destroy 118 | }; 119 | } 120 | 121 | module.exports = { 122 | create: create 123 | }; 124 | -------------------------------------------------------------------------------- /src/utils/fileSystem.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var joinPath; 5 | 6 | var api = context.createInterface("fileSystem", { 7 | copyAll: copyAll, 8 | readDirRecursive: readDirRecursive, 9 | removeRecursive: removeRecursive, 10 | isActualNode: isActualNode 11 | }); 12 | 13 | function init() { 14 | 15 | var getModule = context.channel("getModule"); 16 | var path = getModule("path"); 17 | 18 | joinPath = path.join; 19 | 20 | context.connectInterface(api); 21 | } 22 | 23 | function destroy() { 24 | context.disconnectInterface(api); 25 | } 26 | 27 | function copyAll(inputFs, inputDir, outputFs, outputDir) { 28 | 29 | var nodes = inputFs.readdirSync(inputDir).filter(api.isActualNode); 30 | 31 | nodes.forEach(function (node) { 32 | 33 | var inputPath = joinPath(inputDir, node); 34 | var outputPath = joinPath(outputDir, node); 35 | 36 | if (inputFs.statSync(inputPath).isDirectory()) { 37 | 38 | if (!outputFs.existsSync(outputPath)) { 39 | outputFs.mkdirSync(outputPath); 40 | } 41 | 42 | api.copyAll(inputFs, inputPath, outputFs, outputPath); 43 | } 44 | else { 45 | outputFs.writeFileSync(outputPath, inputFs.readFileSync(inputPath)); 46 | } 47 | }); 48 | } 49 | 50 | function isActualNode(name) { 51 | return name[0] !== '.'; 52 | } 53 | 54 | function readDirRecursive(fs, root, files, prefix) { 55 | 56 | var dir; 57 | 58 | prefix = prefix || ""; 59 | dir = joinPath(root, prefix); 60 | 61 | files = files || []; 62 | 63 | if (!fs.existsSync(dir)) { 64 | return files; 65 | } 66 | 67 | if (fs.statSync(dir).isDirectory()) { 68 | fs.readdirSync(dir).filter(api.isActualNode).forEach(function (name) { 69 | api.readDirRecursive(fs, root, files, joinPath(prefix, name)); 70 | }); 71 | } 72 | else { 73 | files.push(prefix); 74 | } 75 | 76 | return files; 77 | } 78 | 79 | function removeRecursive(fs, dir) { 80 | 81 | var nodes = fs.readdirSync(dir); 82 | 83 | nodes.forEach(function (node) { 84 | 85 | var path; 86 | 87 | if (!api.isActualNode(node)) { 88 | return; 89 | } 90 | 91 | path = joinPath(dir, node); 92 | 93 | if (fs.statSync(path).isDirectory()) { 94 | api.removeRecursive(fs, path); 95 | } 96 | else { 97 | fs.unlinkSync(path); 98 | } 99 | }); 100 | 101 | fs.rmdirSync(dir); 102 | } 103 | 104 | return { 105 | init: init, 106 | destroy: destroy 107 | }; 108 | } 109 | 110 | module.exports = { 111 | create: create 112 | }; 113 | -------------------------------------------------------------------------------- /src/initializer.com.js: -------------------------------------------------------------------------------- 1 | /* global require, __dirname */ 2 | /* eslint-disable camelcase */ 3 | 4 | function create(context) { 5 | 6 | var logger, path, resourceDir, fsHelper; 7 | 8 | var api = context.createInterface("initializer", { 9 | getFolderName: getFolderName, 10 | init: initProject 11 | }); 12 | 13 | function init() { 14 | 15 | var getModule = context.channel("getModule").call; 16 | 17 | path = getModule("path"); 18 | logger = context.getInterface("logger", ["log", "error", "info", "success"]); 19 | fsHelper = context.getInterface("fileSystem", ["copyAll"]); 20 | resourceDir = path.join(__dirname, "/../resources/"); 21 | 22 | context.connectInterface(api); 23 | } 24 | 25 | function destroy() { 26 | logger = null; 27 | context.disconnectInterface(api); 28 | } 29 | 30 | function getFolderName(dirPath) { 31 | 32 | var parts = path.normalize(dirPath + "/").split(path.sep); 33 | 34 | parts.pop(); 35 | 36 | return parts.pop(); 37 | } 38 | 39 | function initProject(fs, dir, then) { 40 | 41 | var name = getFolderName(dir) || "My Toothrot Engine Project"; 42 | 43 | logger.info("Initializing new toothrot project `" + name + "` in `" + dir + "`..."); 44 | 45 | var project = { 46 | name: name, 47 | version: "0.1.0", 48 | electron: { 49 | platform: ["darwin", "linux", "win32"], 50 | version: "1.7.5", 51 | asar: true, 52 | overwrite: true, 53 | prune: false, 54 | tmpdir: false 55 | } 56 | }; 57 | 58 | then = then || function () {}; 59 | dir = path.normalize(dir + "/"); 60 | 61 | if (!fs.existsSync(dir)) { 62 | logger.log("Directory `" + dir + "` doesn't exist; creating..."); 63 | fs.mkdirSync(dir); 64 | } 65 | 66 | logger.info("Copying required resources..."); 67 | 68 | fsHelper.copyAll(fs, resourceDir, fs, dir); 69 | 70 | fs.writeFileSync( 71 | path.join(dir, "toothrot.js"), 72 | fs.readFileSync(path.join(__dirname, "../build", "toothrot.js")) 73 | ); 74 | 75 | logger.success("Resources copied."); 76 | logger.log("Creating `project.json` file..."); 77 | 78 | fs.writeFileSync( 79 | path.normalize(dir + "/project.json"), JSON.stringify(project, null, 4) 80 | ); 81 | 82 | logger.log("Setting story title to project name..."); 83 | 84 | setStoryName(); 85 | 86 | logger.success("Initialized Toothrot Engine project '" + name + "' in " + dir + "."); 87 | 88 | then(null); 89 | 90 | function setStoryName() { 91 | 92 | var file = path.normalize(dir + "/resources/story.trot.md"); 93 | var story = "" + fs.readFileSync(file); 94 | 95 | story = story.replace(/^(\s*)#([^\n]*)/g, "$1# " + name); 96 | 97 | fs.writeFileSync(file, story); 98 | } 99 | } 100 | 101 | return { 102 | init: init, 103 | destroy: destroy 104 | }; 105 | } 106 | 107 | module.exports = { 108 | create: create 109 | }; 110 | -------------------------------------------------------------------------------- /src/runtime/settings.com.js: -------------------------------------------------------------------------------- 1 | 2 | function none() { 3 | // does nothing 4 | } 5 | 6 | function create(context) { 7 | 8 | var storage, story, settings, clone; 9 | 10 | var api = context.createInterface("settings", { 11 | load: load, 12 | save: save, 13 | update: mergeSettings, 14 | remove: remove, 15 | set: set, 16 | has: has, 17 | get: get, 18 | getAll: getAll 19 | }); 20 | 21 | function init() { 22 | 23 | var defaultSettings; 24 | 25 | var getModule = context.channel("getModule").call; 26 | 27 | context.connectInterface(api); 28 | 29 | clone = getModule("clone"); 30 | story = context.getInterface("story", ["getSettings"]); 31 | storage = context.getInterface("storage", ["load", "save"]); 32 | 33 | settings = { 34 | textSpeed: 50, 35 | soundVolume: 100, 36 | ambienceVolume: 100, 37 | musicVolume: 100, 38 | skipMainMenu: false, 39 | continueOnStart: true, 40 | useNextIndicator: true, 41 | useReturnIndicator: true, 42 | indicatorHint: 5000 43 | }; 44 | 45 | defaultSettings = story.getSettings(); 46 | 47 | Object.keys(defaultSettings).forEach(function (key) { 48 | settings[key] = defaultSettings[key]; 49 | }); 50 | 51 | api.load(); 52 | } 53 | 54 | function destroy() { 55 | 56 | context.disconnectInterface(api); 57 | 58 | story = null; 59 | storage = null; 60 | } 61 | 62 | function set(name, value) { 63 | settings[name] = value; 64 | context.publish("update_setting", name); 65 | } 66 | 67 | function remove(name) { 68 | delete settings[name]; 69 | context.publish("remove_setting", name); 70 | } 71 | 72 | function get(name) { 73 | return settings[name]; 74 | } 75 | 76 | function getAll() { 77 | return clone(settings); 78 | } 79 | 80 | function has(name) { 81 | return (name in settings); 82 | } 83 | 84 | function load(then) { 85 | 86 | then = then || none; 87 | 88 | storage.load("settings", function (error, data) { 89 | 90 | if (error) { 91 | return then(error); 92 | } 93 | 94 | if (!data) { 95 | storage.save("settings", settings, function () { 96 | then(); 97 | }); 98 | } 99 | else { 100 | mergeSettings(data.data); 101 | then(); 102 | } 103 | }); 104 | } 105 | 106 | function mergeSettings(other) { 107 | for (var key in other) { 108 | api.set(key, other[key]); 109 | } 110 | } 111 | 112 | function save(then) { 113 | 114 | then = then || none; 115 | 116 | storage.save("settings", api.getAll(), function () { 117 | then(); 118 | }); 119 | } 120 | 121 | return { 122 | init: init, 123 | destroy: destroy 124 | }; 125 | 126 | } 127 | 128 | module.exports = { 129 | name: "settings", 130 | version: "2.0.0", 131 | application: "toothrot", 132 | applicationVersion: "2.x", 133 | applicationSteps: ["run"], 134 | environments: ["any"], 135 | create: create 136 | }; 137 | -------------------------------------------------------------------------------- /src/utils/browser/scrolling.com.js: -------------------------------------------------------------------------------- 1 | 2 | var isLikeGecko = (/like Gecko/i).test(window.navigator.userAgent); 3 | var isGecko = !isLikeGecko && (/Gecko/i).test(window.navigator.userAgent); 4 | // @ts-ignore 5 | var isSafari = (/iPad|iPhone|iPod/).test(window.navigator.userAgent) && !window.MSStream; 6 | 7 | function create(context) { 8 | 9 | var transform, getScrollbarWidth, positioning; 10 | 11 | var api = context.createInterface("uiScrolling", { 12 | hideScrollbar: hideScrollbar, 13 | scrollToBottom: scrollToBottom, 14 | scrollToElement: scrollToElement, 15 | isElementInView: isElementInView 16 | }); 17 | 18 | function init() { 19 | 20 | var getModule = context.channel("getModule").call; 21 | 22 | positioning = context.getInterface("uiPositioning", [ 23 | "getAbsoluteRect", 24 | "getScrollX", 25 | "getScrollY" 26 | ]); 27 | 28 | transform = getModule("transform-js").transform; 29 | getScrollbarWidth = getModule("scrollbarwidth"); 30 | 31 | context.connectInterface(api); 32 | } 33 | 34 | function destroy() { 35 | context.disconnectInterface(api); 36 | } 37 | 38 | function hideScrollbar(element) { 39 | 40 | var scrollbarWidth = getScrollbarWidth(); 41 | 42 | element.style.overflowY = "scroll"; 43 | element.style.overflowX = "hidden"; 44 | element.style.transform = "translate(" + scrollbarWidth + "px, 0px)"; 45 | } 46 | 47 | function scrollToBottom(element, instantly) { 48 | 49 | if (instantly) { 50 | element.scroll(0, element.scrollHeight); 51 | } 52 | else { 53 | // 54 | // Firefox stops smooth scrolling altogether when calling this function repeatedly. 55 | // It has something to do with the 'behavior: "smooth"' part, so we use custom scrolling 56 | // when the browser uses Gecko as rendering engine. 57 | // 58 | if (isGecko || isSafari) { 59 | transform(element.scrollTop, element.scrollHeight, function (v) { 60 | element.scrollTop = v; 61 | }, {duration: 600, easing: "sineOut", fps: 200}); 62 | } 63 | else { 64 | element.scroll({ 65 | top: element.scrollHeight, 66 | left: 0, 67 | behavior: "smooth" 68 | }); 69 | } 70 | } 71 | } 72 | 73 | function scrollToElement(element) { 74 | 75 | if (isElementInView(element)) { 76 | return; 77 | } 78 | 79 | try { 80 | element.scrollIntoView({ 81 | behavior: "smooth" 82 | }); 83 | } 84 | catch (error) { 85 | console.error(error); 86 | } 87 | } 88 | 89 | function isElementInView(element) { 90 | 91 | var rect = positioning.getAbsoluteRect(element); 92 | var scrollX = positioning.getScrollX(); 93 | var scrollY = positioning.getScrollY(); 94 | var xInView = (scrollX <= rect.left) && (rect.left <= (scrollX + window.innerWidth)); 95 | var yInView = (scrollY <= rect.top) && (rect.top <= (scrollY + window.innerHeight)); 96 | 97 | return (xInView && yInView); 98 | } 99 | 100 | return { 101 | init: init, 102 | destroy: destroy 103 | }; 104 | } 105 | 106 | module.exports = { 107 | create: create 108 | }; 109 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 (TBD) 4 | 5 | * Changes to text stream interface (think *Lifeline* series) 6 | * The `objects.json` file and corresponding API removed 7 | * Tags, flags and properties added to nodes 8 | * Nodes can now contain other nodes 9 | * Introduces scripts and slots 10 | * Object links have been removed 11 | * Improved default theme 12 | * Savegame slots removed, using PNG images as "datacards" instead 13 | * Introduces a hierarchy for node tags 14 | * Screens can now have embedded scripts 15 | * Desktop build step has been removed (and will probably return as a plugin) 16 | * Adds `autonext` property 17 | * Makes it possible to define default settings in the main story file 18 | * Internal JS code switched to a component architecture, improving maintainability 19 | * Plugins can now be written to extend almost anything in the engine 20 | * Plugins are discovered automatically during the build phase 21 | 22 | The story format now uses markdown-like syntax where appropriate: 23 | 24 | | v1 Syntax | v2 Syntax | Description | 25 | |:----------------------|:--------------------|:--------------------------------------| 26 | | `#: My Story` | `# My Story` | Story title. | 27 | | `##: Section` | `## Section` | Section ID. | 28 | | `###: Node` | `### Node` | Node ID. | 29 | | `(~~~)` | `***` | Creates anonymous text nodes. | 30 | | `(: foo => bar :)` | `[foo](#bar)` | Links to node `bar` using text `foo`. | 31 | | `(! doSomething() !)` | ``` `@slot` ``` | Executes JS code, inserts result. | 32 | | `($ foo $)` | ``` `$foo` ``` | Inserts a variable `foo`. | 33 | 34 | Various new methods have been added to the env object (`_`) or were changed: 35 | 36 | * `_.event(textOrObject)`: Adds an event. 37 | * `_.last([tag])`: Returns the name of the last node, or if `tag` is given, the last node with 38 | the tag. 39 | * `_.save(savegameId[, then])`: Saves the current state of the game under `savegameId`. 40 | * `_.load(savegameId[, then])`: Loads a savegame. 41 | * `_.notify(text[, type[, duration]])`: Displays a notification. 42 | * `_.dim()`: Has been removed. 43 | 44 | The following new global variables are now available in scripts: 45 | 46 | * `engine`: The toothrot engine context. Can be used to use components directly or to send events 47 | or to listen to them. 48 | * `__file`: The file name of the current script (story file or screen file). 49 | 50 | 51 | ## 1.5.0 (2016-12-09) 52 | 53 | Adds some new methods to the env ("_") object given to a node's JavaScript snippets: 54 | 55 | * _.node() returns the node about to be displayed 56 | * _.addOption(label, target, value) adds a new option to the options menu of the node 57 | 58 | Also introduces these new features: 59 | 60 | * Automatically create an appcache file for browser builds 61 | * Remove browser UI when a game is added to the homescreen 62 | * Add basic ARIA / screen reader support 63 | * Screens can now contain script tags. These are evaluated as if they were inside nodes, 64 | so that `$` and `_` are available there as well, and variables can be changed. 65 | 66 | And improves some things and fixes some bugs: 67 | 68 | * Fix unicode encoding issues 69 | * Using next or back on nodes with options now works, too 70 | * Fixed: Variables not cleared in clearState() 71 | * Show curtain when the section changes 72 | * Disable zoom on mobile devices 73 | * Builder now puts out colored messages on console 74 | * Keyboard highlighter now works for scrolled content 75 | * Replaces move.js with transform.js to fix various glitches with CSS-based animations 76 | * Changes reveal effect to have a minimum speed of 10 chars / second and max 100 chars / second 77 | * Scripts in nodes are now parsed before everything else so that they don't interfere with 78 | toothrot's syntax. 79 | -------------------------------------------------------------------------------- /src/runtime/story.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var story, resources; 5 | 6 | var api = context.createInterface("story", { 7 | getNode: getNode, 8 | hasNode: hasNode, 9 | getSection: getSection, 10 | hasSection: hasSection, 11 | getMeta: getMeta, 12 | hasMeta: hasMeta, 13 | getTitle: getTitle, 14 | getAll: getAll, 15 | getHierarchy: getHierarchy, 16 | getSettings: getSettings, 17 | getGlobalScripts: getGlobalScripts, 18 | hasGlobalScript: hasGlobalScript, 19 | getGlobalScript: getGlobalScript, 20 | getNodeScript: getNodeScript, 21 | hasNodeScript: hasNodeScript, 22 | getSectionScript: getSectionScript, 23 | hasSectionScript: hasSectionScript, 24 | getHead: getHead, 25 | getNodeIds: getNodeIds, 26 | getSectionIds: getSectionIds 27 | }); 28 | 29 | function init() { 30 | context.connectInterface(api); 31 | resources = context.getInterface("resources", ["get", "has"]); 32 | story = resources.get("story"); 33 | } 34 | 35 | function destroy() { 36 | context.disconnectInterface(api); 37 | story = null; 38 | resources = null; 39 | } 40 | 41 | function getNode(name) { 42 | return story.nodes[name]; 43 | } 44 | 45 | function hasNode(name) { 46 | return (name in story.nodes); 47 | } 48 | 49 | function getSection(name) { 50 | return story.sections[name]; 51 | } 52 | 53 | function hasSection(name) { 54 | return (name in story.sections); 55 | } 56 | 57 | function getMeta(name) { 58 | return story.meta[name]; 59 | } 60 | 61 | function hasMeta(name) { 62 | return (name in story.meta); 63 | } 64 | 65 | function getTitle() { 66 | return api.getMeta("title"); 67 | } 68 | 69 | function getAll() { 70 | return story; 71 | } 72 | 73 | function getHierarchy() { 74 | return story.head.hierarchy; 75 | } 76 | 77 | function getSettings() { 78 | return story.head.settings; 79 | } 80 | 81 | function getGlobalScripts() { 82 | return story.head.scripts; 83 | } 84 | 85 | function hasGlobalScript(name) { 86 | return (name in story.head.scripts); 87 | } 88 | 89 | function getGlobalScript(name) { 90 | return story.head.scripts[name]; 91 | } 92 | 93 | function getNodeScript(node, slot) { 94 | 95 | if (!api.hasNodeScript(node, slot)) { 96 | return; 97 | } 98 | 99 | return api.getNode(node).scripts[slot]; 100 | } 101 | 102 | function hasNodeScript(node, slot) { 103 | return api.hasNode(node) && (slot in api.getNode(node).scripts); 104 | } 105 | 106 | function getSectionScript(section, slot) { 107 | 108 | if (!api.hasSectionScript(section, slot)) { 109 | return; 110 | } 111 | 112 | return api.getSection(section).scripts[slot]; 113 | } 114 | 115 | function hasSectionScript(section, slot) { 116 | return api.hasSection(section) && (slot in api.getSection(section).scripts); 117 | } 118 | 119 | function getHead() { 120 | return story.head; 121 | } 122 | 123 | function getNodeIds() { 124 | return Object.keys(story.nodes); 125 | } 126 | 127 | function getSectionIds() { 128 | return Object.keys(story.sections); 129 | } 130 | 131 | return { 132 | init: init, 133 | destroy: destroy 134 | }; 135 | } 136 | 137 | module.exports = { 138 | name: "story", 139 | version: "2.0.0", 140 | application: "toothrot", 141 | applicationVersion: "2.x", 142 | applicationSteps: ["run"], 143 | environments: ["any"], 144 | create: create 145 | }; 146 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global __dirname, process */ 2 | 3 | var fs = require("fs"); 4 | var path = require("path"); 5 | var semver = require("semver"); 6 | var createApp = require("multiversum/bootstrap").bootstrap; 7 | 8 | // @ts-ignore 9 | var package = require("./package.json"); 10 | var TOOTHROT_DIR = __dirname; 11 | 12 | function createToothrotApp(config) { 13 | 14 | var app; 15 | 16 | var resources = {}; 17 | 18 | config = config || {}; 19 | config.applicationStep = config.applicationStep || "build"; 20 | config.environments = Array.isArray(config.environments) ? config.environments : ["node"]; 21 | config.debug = config.debug === true; 22 | config.paths = Array.isArray(config.paths) ? config.paths : []; 23 | 24 | config.paths.unshift(TOOTHROT_DIR); 25 | config.paths.unshift(process.cwd()); 26 | 27 | app = createApp(config.paths, { 28 | patterns: ["**/*.com.json"], 29 | onError: config.debug ? (config.onError || undefined) : (config.onError || function () {}), 30 | filter: function (component, filePath) { 31 | 32 | if (component.application !== "toothrot") { 33 | return false; 34 | } 35 | 36 | if (!semver.satisfies(package.version, component.applicationVersion)) { 37 | return false; 38 | } 39 | 40 | if ( 41 | !("environments" in component) || 42 | !Array.isArray(component.environments) || 43 | !matchesEnvironments(component.environments, config.environments) 44 | ) { 45 | return; 46 | } 47 | 48 | if ( 49 | !config.debug && 50 | // @ts-ignore 51 | Array.isArray(component.flags) && 52 | // @ts-ignore 53 | (!config.debug && component.flags.indexOf("debug") >= 0) 54 | ) { 55 | return false; 56 | } 57 | 58 | if (component.applicationSteps.indexOf(config.applicationStep) >= 0) { 59 | 60 | if (component.resources && typeof component.resources === "object") { 61 | addResources(component.resources, path.dirname(filePath)); 62 | } 63 | 64 | return true; 65 | } 66 | 67 | return false; 68 | } 69 | }); 70 | 71 | app.decorate("getModule", function (fn) { 72 | return function (name) { 73 | 74 | var result = fn(name); 75 | 76 | if (result) { 77 | return result; 78 | } 79 | 80 | return require(name); 81 | }; 82 | }); 83 | 84 | app.connect("getResource", function (name) { 85 | return resources[name]; 86 | }); 87 | 88 | app.connect("getEngineVersion", function () { 89 | return package.version; 90 | }); 91 | 92 | return app; 93 | 94 | function addResources(componentResources, componentDir) { 95 | Object.keys(componentResources).forEach(function (resourceName) { 96 | 97 | var resource = componentResources[resourceName]; 98 | var filePath = typeof resource === "string" ? resource : resource.source; 99 | var fullPath = path.join(componentDir, filePath); 100 | 101 | if (!fs.existsSync(fullPath)) { 102 | throw new Error("Resource cannot be found: " + fullPath); 103 | } 104 | 105 | resources[resourceName] = "" + fs.readFileSync(fullPath); 106 | }); 107 | } 108 | } 109 | 110 | function matchesEnvironments(componentEnvironments, applicationEnvironments) { 111 | return componentEnvironments.some(function (env) { 112 | return applicationEnvironments.indexOf(env) >= 0; 113 | }); 114 | } 115 | 116 | module.exports = { 117 | createApp: createToothrotApp 118 | }; 119 | -------------------------------------------------------------------------------- /src/runtime/browser-ui/options.com.js: -------------------------------------------------------------------------------- 1 | // 2 | // # Options Browser UI Component 3 | // 4 | // This component adds the *options* feature to the browser UI (`ui` component). 5 | // 6 | 7 | var CHANNEL_UI_INSERT_CONTROLS = "ui/insertNodeControls"; 8 | var CHANNEL_UI_HAS_CONTROLS = "ui/nodeHasControls"; 9 | 10 | function create(context) { 11 | 12 | var container, wrapper; 13 | 14 | var api = context.createInterface("uiOptions", { 15 | add: addOption, 16 | addMany: addOptions, 17 | createWrapper: createWrapper, 18 | createContainer: createContainer 19 | }); 20 | 21 | function init() { 22 | 23 | context.connectInterface(api); 24 | 25 | // Options are put into a wrapper element 26 | // so that clicks can be intercepted and to allow 27 | // more flexibility in styling the elements with CSS. 28 | wrapper = api.createWrapper(); 29 | container = api.createContainer(); 30 | 31 | wrapper.appendChild(container); 32 | 33 | context.decorate(CHANNEL_UI_INSERT_CONTROLS, decorateNodeControls); 34 | context.decorate(CHANNEL_UI_HAS_CONTROLS, decorateHasNodeControls); 35 | } 36 | 37 | function destroy() { 38 | 39 | context.disconnectInterface(api); 40 | context.removeDecorator(CHANNEL_UI_INSERT_CONTROLS, decorateNodeControls); 41 | context.removeDecorator(CHANNEL_UI_HAS_CONTROLS, decorateHasNodeControls); 42 | 43 | api = null; 44 | wrapper = null; 45 | container = null; 46 | } 47 | 48 | function decorateNodeControls(fn) { 49 | return function (nodeElement, node) { 50 | 51 | var result = fn.apply(null, arguments); 52 | 53 | if (node.options.length) { 54 | api.addMany(nodeElement, node); 55 | } 56 | 57 | return result; 58 | }; 59 | } 60 | 61 | function decorateHasNodeControls(fn) { 62 | return function (node) { 63 | return node.options && node.options.length ? true : fn.apply(null, arguments); 64 | }; 65 | } 66 | 67 | function createWrapper() { 68 | 69 | var wrapper = document.createElement("div"); 70 | 71 | wrapper.setAttribute("class", "options-curtain"); 72 | wrapper.addEventListener("click", onWrapperClick); 73 | 74 | return wrapper; 75 | } 76 | 77 | function createContainer() { 78 | 79 | var container = document.createElement("div"); 80 | 81 | container.setAttribute("class", "options-container"); 82 | 83 | return container; 84 | } 85 | 86 | function addOptions(nodeElement, node) { 87 | 88 | container.innerHTML = ""; 89 | 90 | node.options.forEach(function (option) { 91 | api.add(option, node); 92 | }); 93 | 94 | nodeElement.appendChild(wrapper); 95 | } 96 | 97 | function addOption(opt) { 98 | 99 | var option = document.createElement("span"); 100 | 101 | option.setAttribute("class", "option"); 102 | option.setAttribute("data-type", "option"); 103 | option.setAttribute("data-target", opt.target); 104 | option.setAttribute("data-focus-mode", "node"); 105 | option.setAttribute("tabindex", "1"); 106 | option.setAttribute("data-value", window.btoa(JSON.stringify(opt.value))); 107 | 108 | option.innerHTML = opt.label; 109 | 110 | container.appendChild(option); 111 | } 112 | 113 | function onWrapperClick(event) { 114 | if (event.target.getAttribute("data-type") !== "option") { 115 | event.stopPropagation(); 116 | event.preventDefault(); 117 | } 118 | } 119 | 120 | return { 121 | init: init, 122 | destroy: destroy 123 | }; 124 | } 125 | 126 | module.exports = { 127 | create: create 128 | }; 129 | -------------------------------------------------------------------------------- /src/packer.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var parser, toDataUri, normalize, storyFileReader; 5 | 6 | var api = context.createInterface("packer", { 7 | pack: pack, 8 | pruneStory: pruneStory 9 | }); 10 | 11 | function init() { 12 | 13 | var getModule = context.channel("getModule").call; 14 | 15 | toDataUri = getModule("datauri").sync; 16 | normalize = getModule("path").normalize; 17 | parser = context.getInterface("parser", ["parse"]); 18 | storyFileReader = context.getInterface("storyFileReader", ["read"]); 19 | 20 | context.connectInterface(api); 21 | } 22 | 23 | function destroy() { 24 | 25 | parser = null; 26 | storyFileReader = null; 27 | 28 | context.disconnectInterface(api); 29 | } 30 | 31 | function pack(fs, dir, then) { 32 | 33 | var story, storyFilesContent, templatePath, screenPath, imagePath, astFile, templateFiles, 34 | screenFiles, imageFiles, bundle, errors; 35 | 36 | dir = normalize(dir + "/resources/"); 37 | templatePath = normalize(dir + "/templates/"); 38 | screenPath = normalize(dir + "/screens/"); 39 | imagePath = normalize(dir + "/images/"); 40 | astFile = normalize(dir + "/ast.json"); 41 | templateFiles = fs.readdirSync(templatePath); 42 | screenFiles = fs.readdirSync(screenPath); 43 | imageFiles = fs.readdirSync(imagePath); 44 | 45 | // 46 | // If there's an AST file in the resources folder (created by e.g. toothrot builder) 47 | // we use it instead of parsing the story file. 48 | // 49 | if (fs.existsSync(astFile)) { 50 | story = JSON.parse("" + fs.readFileSync(astFile)); 51 | } 52 | else { 53 | 54 | storyFilesContent = storyFileReader.read(fs, dir); 55 | 56 | parser.parse("" + storyFilesContent, function (storyErrors, ast) { 57 | if (storyErrors) { 58 | errors = storyErrors; 59 | } 60 | else { 61 | story = ast; 62 | } 63 | }); 64 | 65 | if (errors) { 66 | return then(errors); 67 | } 68 | } 69 | 70 | bundle = { 71 | meta: { 72 | buildTime: Date.now() 73 | }, 74 | templates: {}, 75 | screens: {}, 76 | images: {}, 77 | story: story 78 | }; 79 | 80 | templateFiles.forEach(function (file) { 81 | 82 | var name = file.split(".")[0]; 83 | 84 | bundle.templates[name] = "" + fs.readFileSync(templatePath + file); 85 | }); 86 | 87 | screenFiles.forEach(function (file) { 88 | 89 | var name = file.split(".")[0]; 90 | 91 | bundle.screens[name] = "" + fs.readFileSync(screenPath + file); 92 | }); 93 | 94 | imageFiles.forEach(function (file) { 95 | 96 | var name = "images/" + file; 97 | 98 | bundle.images[name] = toDataUri(imagePath + file); 99 | }); 100 | 101 | api.pruneStory(story); 102 | 103 | return then(null, JSON.stringify(bundle, null, 4)); 104 | } 105 | 106 | function pruneStory(story) { 107 | 108 | Object.keys(story.nodes).forEach(function (key) { 109 | delete story.nodes[key].raw; 110 | }); 111 | 112 | Object.keys(story.sections).forEach(function (key) { 113 | delete story.sections[key].raw; 114 | }); 115 | 116 | delete story.head.content; 117 | } 118 | 119 | return { 120 | init: init, 121 | destroy: destroy 122 | }; 123 | } 124 | 125 | module.exports = { 126 | create: create 127 | }; 128 | -------------------------------------------------------------------------------- /src/scriptExporter.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var format, template, scriptTemplate; 5 | 6 | var api = context.createInterface("scriptExporter", { 7 | render: render, 8 | renderScript: renderScript, 9 | renderScripts: renderScripts, 10 | renderNodeScript: renderNodeScript, 11 | renderGlobalScript: renderGlobalScript, 12 | renderSectionScript: renderSectionScript, 13 | getScriptFileDescription: getScriptFileDescription 14 | }); 15 | 16 | function init() { 17 | 18 | var getModule = context.channel("getModule"); 19 | var getResource = context.channel("getResource"); 20 | 21 | format = getModule("vrep").create("{{$", "}}"); 22 | template = getResource("scriptsTemplate"); 23 | scriptTemplate = getResource("scriptTemplate"); 24 | 25 | context.connectInterface(api); 26 | } 27 | 28 | function destroy() { 29 | 30 | format = null; 31 | template = null; 32 | scriptTemplate = null; 33 | 34 | context.disconnectInterface(api); 35 | } 36 | 37 | function getScriptFileDescription(story) { 38 | return "Exported scripts for Toothrot Engine project `" + story.meta.title + "`."; 39 | } 40 | 41 | function render(story) { 42 | 43 | var functions = api.renderScripts(story); 44 | 45 | return format(template, { 46 | description: api.getScriptFileDescription(story), 47 | engineVersion: context.channel("getEngineVersion")(), 48 | buildTime: (new Date()).toUTCString(), 49 | functions: functions 50 | }); 51 | } 52 | 53 | function renderScripts(story) { 54 | 55 | var functions = ""; 56 | 57 | functions += Object.keys(story.head.scripts).map(function (slotName) { 58 | return api.renderGlobalScript(story, slotName); 59 | }).join("\n"); 60 | 61 | functions += Object.keys(story.nodes).map(function (nodeName) { 62 | 63 | var node = story.nodes[nodeName]; 64 | 65 | return Object.keys(node.scripts).map(function (slotName) { 66 | return api.renderNodeScript(story, node, slotName); 67 | }).join("\n"); 68 | }).join("\n"); 69 | 70 | functions += Object.keys(story.sections).map(function (sectionName) { 71 | 72 | var section = story.sections[sectionName]; 73 | 74 | return Object.keys(section.scripts).map(function (slotName) { 75 | return api.renderSectionScript(story, section, slotName); 76 | }).join("\n"); 77 | }).join("\n"); 78 | 79 | return functions; 80 | } 81 | 82 | function renderNodeScript(story, node, slotName) { 83 | 84 | var script = node.scripts[slotName]; 85 | 86 | return api.renderScript( 87 | "Script for slot `" + slotName +"` in node `" + node.id + "`.", 88 | "node_" + node.id, 89 | script 90 | ); 91 | } 92 | 93 | function renderSectionScript(story, section, slotName) { 94 | 95 | var script = section.scripts[slotName]; 96 | 97 | return api.renderScript( 98 | "Script for slot `" + slotName +"` in section `" + section.id + "`.", 99 | "section_" + section.id, 100 | script 101 | ); 102 | } 103 | 104 | function renderGlobalScript(story, slotName) { 105 | 106 | var script = story.head.scripts[slotName]; 107 | 108 | return api.renderScript("Script for global slot `" + slotName + "`.", "global", script); 109 | } 110 | 111 | function renderScript(description, prefix, script) { 112 | return format(scriptTemplate, { 113 | description: description, 114 | file: script.file, 115 | line: script.line, 116 | functionName: prefix + "__slot_" + script.slot, 117 | functionBody: script.body 118 | }); 119 | } 120 | 121 | return { 122 | init: init, 123 | destroy: destroy 124 | }; 125 | } 126 | 127 | module.exports = { 128 | create: create 129 | }; 130 | -------------------------------------------------------------------------------- /src/utils/browser/revealEffect.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var transform; 5 | 6 | var api = context.createInterface("uiRevealEffect", { 7 | create: createEffect, 8 | markCharacters: markCharacters, 9 | hideCharacters: hideCharacters, 10 | setOpacity: setOpacity 11 | }); 12 | 13 | function init() { 14 | transform = context.channel("getModule").call("transform-js").transform; 15 | context.connectInterface(api); 16 | } 17 | 18 | function destroy() { 19 | context.disconnectInterface(api); 20 | } 21 | 22 | function createEffect(element, speed, then) { 23 | 24 | var chars, left; 25 | var offset = 1000 / (speed || 40); 26 | var stop = false; 27 | var timeouts = []; 28 | 29 | api.markCharacters(element); 30 | api.hideCharacters(element); 31 | 32 | chars = element.querySelectorAll(".char"); 33 | left = chars.length; 34 | 35 | then = then || function () {}; 36 | 37 | function start() { 38 | 39 | [].forEach.call(chars, function (char, i) { 40 | 41 | var id = setTimeout(function () { 42 | 43 | if (stop) { 44 | return; 45 | } 46 | 47 | transform(0, 1, api.setOpacity(char), {duration: 10 * offset}, function () { 48 | 49 | left -= 1; 50 | 51 | if (stop) { 52 | return; 53 | } 54 | 55 | if (left <= 0) { 56 | then(); 57 | } 58 | 59 | }); 60 | 61 | }, i * offset); 62 | 63 | timeouts.push(id); 64 | }); 65 | } 66 | 67 | function cancel() { 68 | 69 | if (stop || left <= 0) { 70 | return false; 71 | } 72 | 73 | stop = true; 74 | 75 | timeouts.forEach(function (id) { 76 | clearTimeout(id); 77 | }); 78 | 79 | [].forEach.call(chars, function (char) { 80 | char.style.opacity = "1"; 81 | }); 82 | 83 | then(); 84 | 85 | return true; 86 | } 87 | 88 | return { 89 | start: start, 90 | cancel: cancel 91 | }; 92 | } 93 | 94 | function hideCharacters(element) { 95 | 96 | var chars = element.querySelectorAll(".char"); 97 | 98 | [].forEach.call(chars, function (char) { 99 | char.style.opacity = 0; 100 | }); 101 | } 102 | 103 | function markCharacters(element, offset) { 104 | 105 | var TEXT_NODE = 3; 106 | var ELEMENT = 1; 107 | 108 | offset = offset || 0; 109 | 110 | [].forEach.call(element.childNodes, function (child) { 111 | 112 | var text = "", newNode; 113 | 114 | if (child.nodeType === TEXT_NODE) { 115 | 116 | [].forEach.call(child.textContent, function (char) { 117 | text += '' + char + ''; 118 | offset += 1; 119 | }); 120 | 121 | newNode = document.createElement("span"); 122 | 123 | newNode.setAttribute("class", "char-container"); 124 | 125 | newNode.innerHTML = text; 126 | 127 | child.parentNode.replaceChild(newNode, child); 128 | } 129 | else if (child.nodeType === ELEMENT) { 130 | offset = api.markCharacters(child, offset); 131 | } 132 | }); 133 | 134 | return offset; 135 | } 136 | 137 | function setOpacity(element) { 138 | return function (v) { 139 | element.style.opacity = v; 140 | }; 141 | } 142 | 143 | return { 144 | init: init, 145 | destroy: destroy 146 | }; 147 | } 148 | 149 | module.exports = { 150 | create: create 151 | }; 152 | -------------------------------------------------------------------------------- /src/runtime/system.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var features, runningElectron, remote, logger, clone; 5 | 6 | var system = context.createInterface("system", { 7 | exit: exit, 8 | canExit: canExit, 9 | fullscreen: fullscreen, 10 | getFeatures: getFeatures, 11 | hasFullscreen: hasFullscreen, 12 | hasBrowserFullscreen: hasBrowserFullscreen, 13 | toggleFullscreen: toggleFullscreen, 14 | enterFullscreen: fullscreen, 15 | exitFullscreen: exitFullscreen, 16 | fullscreenEnabled: fullscreenEnabled, 17 | requestFullscreen: requestFullscreen, 18 | exitBrowserFullscreen: exitBrowserFullscreen 19 | }); 20 | 21 | function init() { 22 | 23 | var getModule = context.channel("getModule").call; 24 | 25 | clone = getModule("clone"); 26 | 27 | context.connectInterface(system); 28 | 29 | logger = context.getInterface("logger", ["log", "error"]); 30 | 31 | runningElectron = typeof window !== "undefined" && 32 | // @ts-ignore 33 | typeof window.process === "object" && 34 | // @ts-ignore 35 | window.process.type === "renderer"; 36 | 37 | if (runningElectron) { 38 | // @ts-ignore 39 | remote = window.require("electron").remote; 40 | } 41 | 42 | features = { 43 | fullscreen: system.hasFullscreen(), 44 | exit: system.canExit() 45 | }; 46 | } 47 | 48 | function destroy() { 49 | features = null; 50 | context.disconnectInterface(system); 51 | } 52 | 53 | function getFeatures() { 54 | return clone(features); 55 | } 56 | 57 | function toggleFullscreen() { 58 | 59 | var fullscreenOn = system.fullscreenEnabled(); 60 | 61 | if (fullscreenOn) { 62 | system.exitFullscreen(); 63 | } 64 | else { 65 | system.fullscreen(); 66 | } 67 | 68 | context.publish("fullscreen_change", !fullscreenOn); 69 | } 70 | 71 | function fullscreenEnabled() { 72 | 73 | if (runningElectron) { 74 | return remote.getCurrentWindow().isFullScreen(); 75 | } 76 | 77 | return document.fullscreenElement || 78 | // @ts-ignore 79 | document.mozFullScreenElement || 80 | // @ts-ignore 81 | document.msFullscreenElement || 82 | document.webkitFullscreenElement; 83 | } 84 | 85 | function fullscreen() { 86 | if (runningElectron) { 87 | remote.getCurrentWindow().setFullScreen(true); 88 | } 89 | else { 90 | system.requestFullscreen(document.body.parentNode); 91 | } 92 | } 93 | 94 | function exitFullscreen() { 95 | if (runningElectron) { 96 | remote.getCurrentWindow().setFullScreen(false); 97 | } 98 | else { 99 | system.exitBrowserFullscreen(); 100 | } 101 | } 102 | 103 | function exitBrowserFullscreen() { 104 | if (document.exitFullscreen) { 105 | document.exitFullscreen(); 106 | } 107 | else if ("msExitFullscreen" in document) { 108 | // @ts-ignore 109 | document.msExitFullscreen(); 110 | } 111 | else if ("mozCancelFullScreen" in document) { 112 | // @ts-ignore 113 | document.mozCancelFullScreen(); 114 | } 115 | else if (document.webkitExitFullscreen) { 116 | document.webkitExitFullscreen(); 117 | } 118 | } 119 | 120 | function requestFullscreen(element) { 121 | if (element.requestFullscreen) { 122 | element.requestFullscreen(); 123 | } 124 | else if (element.msRequestFullscreen) { 125 | element.msRequestFullscreen(); 126 | } 127 | else if (element.mozRequestFullScreen) { 128 | element.mozRequestFullScreen(); 129 | } 130 | else if (element.webkitRequestFullscreen) { 131 | element.webkitRequestFullscreen(); 132 | } 133 | } 134 | 135 | function hasBrowserFullscreen() { 136 | return "requestFullscreen" in document.documentElement || 137 | "msRequestFullscreen" in document.documentElement || 138 | "mozRequestFullScreen" in document.documentElement || 139 | "webkitRequestFullscreen" in document.documentElement; 140 | } 141 | 142 | function hasFullscreen() { 143 | return runningElectron || system.hasBrowserFullscreen(); 144 | } 145 | 146 | function canExit() { 147 | return runningElectron; 148 | } 149 | 150 | function exit() { 151 | 152 | try { 153 | remote.getCurrentWindow().close(); 154 | } 155 | catch (error) { 156 | logger.error("Cannot exit: " + error); 157 | } 158 | } 159 | 160 | return { 161 | init: init, 162 | destroy: destroy 163 | }; 164 | } 165 | 166 | module.exports = { 167 | name: "system", 168 | version: "2.0.0", 169 | application: "toothrot", 170 | applicationVersion: "2.x", 171 | applicationSteps: ["run"], 172 | environments: ["browser"], 173 | create: create 174 | }; 175 | -------------------------------------------------------------------------------- /src/runtime/browser-ui/highlighter.com.js: -------------------------------------------------------------------------------- 1 | // 2 | // # Highlighter Component 3 | // 4 | // The highlighter is an absolutely positioned element that can be moved over 5 | // clickable elements by using the arrow keys. Hitting the return key when an element 6 | // is highlighted will execute a click on the element. 7 | // 8 | 9 | function createStyleSetter(element) { 10 | return function (key, unit, start, end) { 11 | return function (value) { 12 | element.style[key] = (start + (value * (end - start))) + unit; 13 | return value; 14 | }; 15 | }; 16 | } 17 | 18 | function create(context) { 19 | 20 | var highlighterElement, focus, scrolling, getAbsoluteRect, transform; 21 | 22 | var highlighter = context.createInterface("highlighter", { 23 | highlight: highlight, 24 | highlightCurrent: highlightCurrent, 25 | reset: reset, 26 | onClick: onClick 27 | }); 28 | 29 | function init() { 30 | 31 | var getModule = context.channel("getModule").call; 32 | 33 | transform = getModule("transform-js").transform; 34 | getAbsoluteRect = context.channel("uiPositioning/getAbsoluteRect"); 35 | 36 | context.connectInterface(highlighter); 37 | 38 | scrolling = context.getInterface("uiScrolling", ["scrollToElement"]); 39 | focus = context.getInterface("focus", ["execute", "getElementInFocus"]); 40 | highlighterElement = document.createElement("div"); 41 | 42 | highlighterElement.setAttribute("class", "highlighter"); 43 | highlighterElement.setAttribute("data-type", "highlighter"); 44 | 45 | highlighterElement.addEventListener("click", onClick); 46 | 47 | document.body.appendChild(highlighterElement); 48 | 49 | context.on("timer_end", highlighter.reset); 50 | context.on("screen_exit", highlighter.reset); 51 | context.on("screen_entry", highlighter.reset); 52 | context.on("change_focus_mode", highlighter.reset); 53 | context.on("focus_change", highlighter.highlight); 54 | context.on("element_reflow", highlighter.highlightCurrent); 55 | } 56 | 57 | function destroy() { 58 | 59 | context.disconnectInterface(highlighter); 60 | 61 | highlighterElement.removeEventListener("click", highlighter.onClick); 62 | highlighterElement.parentNode.removeChild(highlighterElement); 63 | 64 | context.removeListener("timer_end", highlighter.reset); 65 | context.removeListener("screen_exit", highlighter.reset); 66 | context.removeListener("screen_entry", highlighter.reset); 67 | context.removeListener("change_focus_mode", highlighter.reset); 68 | context.removeListener("focus_change", highlighter.highlight); 69 | context.removeListener("element_reflow", highlighter.highlightCurrent); 70 | 71 | highlighterElement = null; 72 | focus = null; 73 | } 74 | 75 | function onClick(event) { 76 | event.stopPropagation(); 77 | event.preventDefault(); 78 | focus.execute(); 79 | } 80 | 81 | function highlightCurrent() { 82 | 83 | var element = focus.getElementInFocus(); 84 | 85 | if (element) { 86 | highlighter.highlight(element); 87 | } 88 | } 89 | 90 | function highlight(element) { 91 | 92 | var padding = 1; 93 | var sourceRect = getAbsoluteRect(highlighterElement); 94 | var targetRect = getAbsoluteRect(element); 95 | var setHighlighterStyle = createStyleSetter(highlighterElement); 96 | 97 | var left = targetRect.left - padding; 98 | var top = targetRect.top - padding; 99 | var width = targetRect.width + (2 * padding); 100 | var height = targetRect.height + (2 * padding); 101 | var currentOpacity = +highlighterElement.style.opacity || 0; 102 | 103 | var setX = setHighlighterStyle("left", "px", sourceRect.left, left); 104 | var setY = setHighlighterStyle("top", "px", sourceRect.top, top); 105 | var setWidth = setHighlighterStyle("width", "px", sourceRect.width, width); 106 | var setHeight = setHighlighterStyle("height", "px", sourceRect.height, height); 107 | var setOpacity = setHighlighterStyle("opacity", "", currentOpacity, 1); 108 | 109 | function setValues(value) { 110 | return setOpacity(setHeight(setWidth(setY(setX(value))))); 111 | } 112 | 113 | transform(0, 1, setValues, {duration: 200, fps: 60}); 114 | 115 | setTimeout(function () { 116 | scrolling.scrollToElement(element); 117 | }, 10); 118 | } 119 | 120 | function reset() { 121 | 122 | var setHighlighterStyle = createStyleSetter(highlighterElement); 123 | var sourceRect = getAbsoluteRect(highlighterElement); 124 | 125 | var setX = setHighlighterStyle("left", "px", sourceRect.left, 0); 126 | var setY = setHighlighterStyle("top", "px", sourceRect.top, 0); 127 | var setWidth = setHighlighterStyle("width", "px", sourceRect.width, 0); 128 | var setHeight = setHighlighterStyle("height", "px", sourceRect.height, 0); 129 | 130 | var setOpacity = setHighlighterStyle( 131 | "opacity", 132 | "", 133 | (+highlighterElement.style.opacity || 0), 134 | 0 135 | ); 136 | 137 | function setValues(value) { 138 | return setOpacity(setHeight(setWidth(setY(setX(value))))); 139 | } 140 | 141 | transform(0, 1, setValues, {duration: 200, fps: 60}); 142 | } 143 | 144 | return { 145 | init: init, 146 | destroy: destroy 147 | }; 148 | } 149 | 150 | module.exports = { 151 | create: create 152 | }; 153 | -------------------------------------------------------------------------------- /src/runtime/storage.com.js: -------------------------------------------------------------------------------- 1 | // 2 | // Module for storing the game state in local storage. 3 | // 4 | // Savegames look like this: 5 | // 6 | // 7 | // { 8 | // name: "fooBarBaz", // a name. will be given by the engine 9 | // time: 012345678 // timestamp - this must be set by the storage 10 | // data: {} // this is what the engine gives the storage 11 | // } 12 | 13 | var none = function () {}; 14 | 15 | function create(context) { 16 | 17 | var storageType, storageKey, getItem, setItem, logger; 18 | var memoryStorage = Object.create(null); 19 | var testStorageKey = "TOOTHROT-storage-test"; 20 | 21 | var api = context.createInterface("storage", { 22 | save: save, 23 | load: load, 24 | all: all, 25 | remove: remove, 26 | getStorageKey: getStorageKey 27 | }); 28 | 29 | try { 30 | 31 | localStorage.setItem(testStorageKey, "works"); 32 | 33 | if (localStorage.getItem(testStorageKey) === "works") { 34 | storageType = "local"; 35 | } 36 | } 37 | catch (error) { 38 | console.warn(error); 39 | } 40 | 41 | if (!storageType) { 42 | 43 | try { 44 | 45 | sessionStorage.setItem(testStorageKey, "works"); 46 | 47 | if (sessionStorage.getItem(testStorageKey) === "works") { 48 | storageType = "session"; 49 | } 50 | } 51 | catch (error) { 52 | console.warn(error); 53 | } 54 | } 55 | 56 | if (!storageType) { 57 | storageType = "memory"; 58 | } 59 | 60 | if (storageType === "local") { 61 | 62 | getItem = function (name) { 63 | return JSON.parse(localStorage.getItem(name) || "{}"); 64 | }; 65 | 66 | setItem = function (name, value) { 67 | return localStorage.setItem(name, JSON.stringify(value)); 68 | }; 69 | } 70 | else if (storageType === "session") { 71 | 72 | getItem = function (name) { 73 | return JSON.parse(sessionStorage.getItem(name) || "{}"); 74 | }; 75 | 76 | setItem = function (name, value) { 77 | return sessionStorage.setItem(name, JSON.stringify(value)); 78 | }; 79 | } 80 | else { 81 | 82 | getItem = function (name) { 83 | return JSON.parse(memoryStorage[name] || "{}"); 84 | }; 85 | 86 | setItem = function (name, value) { 87 | return memoryStorage[name] = JSON.stringify(value); 88 | }; 89 | } 90 | 91 | function init() { 92 | 93 | context.connectInterface(api); 94 | 95 | logger = context.getInterface("logger", ["log", "error"]); 96 | 97 | // Each story should have its own storage key so that 98 | // one story doesn't overwrite another story's savegames 99 | // and settings. 100 | storageKey = api.getStorageKey(); 101 | } 102 | 103 | function destroy() { 104 | context.disconnectInterface(api); 105 | } 106 | 107 | function getStorageKey() { 108 | 109 | var story = context.getInterface("story", ["getTitle"]); 110 | 111 | return "TOOTHROT-" + story.getTitle(); 112 | } 113 | 114 | function save(name, data, then) { 115 | 116 | var store, error; 117 | 118 | then = then || none; 119 | 120 | try { 121 | 122 | store = getItem(storageKey); 123 | 124 | store[name] = { 125 | name: name, 126 | time: Date.now(), 127 | data: data 128 | }; 129 | 130 | setItem(storageKey, store); 131 | 132 | } 133 | catch (e) { 134 | logger.error(e); 135 | error = e; 136 | } 137 | 138 | if (error) { 139 | return then(error); 140 | } 141 | 142 | then(null, true); 143 | } 144 | 145 | function load(name, then) { 146 | 147 | var value, error; 148 | 149 | then = then || none; 150 | 151 | try { 152 | value = getItem(storageKey)[name]; 153 | } 154 | catch (e) { 155 | logger.error(e); 156 | error = e; 157 | } 158 | 159 | if (error) { 160 | return then(error); 161 | } 162 | 163 | then(null, value); 164 | } 165 | 166 | function all(then) { 167 | 168 | var value, error; 169 | 170 | then = then || none; 171 | 172 | try { 173 | value = getItem(storageKey); 174 | } 175 | catch (e) { 176 | logger.error(e); 177 | error = e; 178 | } 179 | 180 | if (error) { 181 | return then(error); 182 | } 183 | 184 | then(null, value); 185 | } 186 | 187 | function remove(name, then) { 188 | 189 | var value, error; 190 | 191 | then = then || none; 192 | 193 | try { 194 | value = getItem(storageKey); 195 | } 196 | catch (e) { 197 | logger.error(e); 198 | error = e; 199 | } 200 | 201 | if (error) { 202 | return then(error); 203 | } 204 | 205 | delete value[name]; 206 | 207 | setItem(storageKey, value); 208 | 209 | then(null, true); 210 | } 211 | 212 | return { 213 | init: init, 214 | destroy: destroy 215 | }; 216 | } 217 | 218 | module.exports = { 219 | name: "storage", 220 | version: "2.0.0", 221 | application: "toothrot", 222 | applicationVersion: "2.x", 223 | applicationSteps: ["run"], 224 | environments: ["browser"], 225 | create: create 226 | }; 227 | -------------------------------------------------------------------------------- /src/gatherer.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var path, createHost, createGatherer, createFormatter, format; 5 | 6 | var api = context.createInterface("toothrotGatherer", { 7 | gather: gather, 8 | resolveResources: resolveResources, 9 | renderTemplate: renderTemplate 10 | }); 11 | 12 | function init() { 13 | 14 | var getModule = context.channel("getModule"); 15 | 16 | path = getModule("path"); 17 | createHost = getModule("multiversum/host").create; 18 | createGatherer = getModule("multiversum/gatherer").create; 19 | createFormatter = getModule("vrep").create; 20 | 21 | format = createFormatter("'{{$", "}}'", function (value) { 22 | return value; 23 | }); 24 | 25 | context.connectInterface(api); 26 | } 27 | 28 | function destroy() { 29 | context.disconnectInterface(api); 30 | } 31 | 32 | function gather(options) { 33 | 34 | var gatherer, components; 35 | var resources = {}; 36 | var moduleNames = []; 37 | var host = createHost(); 38 | 39 | host.connect("getModule", function () { 40 | return null; 41 | }); 42 | 43 | options.prepareHost(host); 44 | 45 | gatherer = createGatherer(host); 46 | 47 | gatherer.init(); 48 | 49 | components = gatherer.gather(options.paths, { 50 | patterns: ["**/*.com.json"], 51 | filter: function (component, componentPath) { 52 | 53 | if (component.application !== "toothrot") { 54 | return false; 55 | } 56 | 57 | if (component.applicationSteps.indexOf("run") < 0) { 58 | return false; 59 | } 60 | 61 | if (Array.isArray(component.modules)) { 62 | component.modules.forEach(function (name) { 63 | if (moduleNames.indexOf(name) < 0) { 64 | moduleNames.push(name); 65 | } 66 | }); 67 | } 68 | 69 | if (component.resources && typeof component.resources === "object") { 70 | Object.keys(component.resources).forEach(function (key) { 71 | 72 | resources[key] = path.join( 73 | path.dirname(componentPath), 74 | component.resources[key] 75 | ); 76 | 77 | }); 78 | } 79 | 80 | return true; 81 | } 82 | }); 83 | 84 | return { 85 | resources: resources, 86 | dependencies: moduleNames, 87 | components: components 88 | }; 89 | 90 | } 91 | 92 | function resolveResources(fs, resources) { 93 | 94 | var count = 0; 95 | var files = {}; 96 | var masked = {}; 97 | var resolved = {}; 98 | 99 | Object.keys(resources).forEach(function (key) { 100 | 101 | var id; 102 | var filePath = resources[key]; 103 | 104 | if (filePath in files) { 105 | resolved[key] = files[filePath]; 106 | return; 107 | } 108 | 109 | id = getNextId(filePath); 110 | 111 | masked[id] = "" + fs.readFileSync(filePath); 112 | files[filePath] = id; 113 | resolved[key] = id; 114 | }); 115 | 116 | return { 117 | files: masked, 118 | resources: resolved 119 | }; 120 | 121 | function getNextId(filePath) { 122 | 123 | var id = "/" + path.basename(filePath); 124 | 125 | count += 1; 126 | 127 | return id; 128 | } 129 | } 130 | 131 | function renderTemplate(fs, template, componentTemplate, options) { 132 | 133 | var collected = gather(options); 134 | 135 | var dependencies = {}; 136 | var componentContents = {}; 137 | var resources = api.resolveResources(fs, collected.resources); 138 | 139 | collected.dependencies.forEach(function (name) { 140 | dependencies[name] = "require('" + name + "')"; 141 | }); 142 | 143 | Object.keys(collected.components).forEach(function (key) { 144 | 145 | var id = ""; 146 | var component = collected.components[key]; 147 | var filePath = path.join(id, path.basename(component.file)); 148 | var componentContent = "" + fs.readFileSync(component.file); 149 | 150 | componentContents[id] = wrapComponentContent(componentContent); 151 | 152 | // @ts-ignore 153 | component.create = "{{$" + id + "}}"; 154 | component.file = filePath; 155 | component.definitionFile = path.join(id, path.basename(component.definitionFile)); 156 | }); 157 | 158 | return format(template, { 159 | buildTime: (new Date()).toUTCString(), 160 | mods: insertRequires(JSON.stringify(dependencies, null, 4)), 161 | components: formatComponents(collected.components, componentContents), 162 | resourceIds: JSON.stringify(resources.resources, null, 4), 163 | resourceFiles: JSON.stringify(resources.files, null, 4) 164 | }); 165 | 166 | function insertRequires(input) { 167 | return input.replace(/"require\((.*?)\)"/g, "require($1)"); 168 | } 169 | 170 | function wrapComponentContent(content) { 171 | return format(componentTemplate, { 172 | content: content 173 | }); 174 | } 175 | 176 | function formatComponents(components, contents) { 177 | 178 | var format = createFormatter('"{{$', '}}"'); 179 | 180 | return format(JSON.stringify(components, null, 4), contents); 181 | } 182 | 183 | } 184 | 185 | return { 186 | init: init, 187 | destroy: destroy 188 | }; 189 | } 190 | 191 | module.exports = { 192 | create: create 193 | }; 194 | -------------------------------------------------------------------------------- /src/runtime/audio.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var vars, settings, currentAmbience, currentMusic, currentSound, Howl; 5 | 6 | var audio = context.createInterface("audio", { 7 | serializePath: serializeAudioPath, 8 | unserializePath: unserializeAudioPath, 9 | getPaths: getAudioPaths, 10 | play: playTrack, 11 | stop: stopAudio 12 | }); 13 | 14 | var ambience = context.createInterface("ambience", { 15 | play: playAmbience, 16 | stop: stopAmbience 17 | }); 18 | 19 | var music = context.createInterface("music", { 20 | play: playMusic, 21 | stop: stopMusic 22 | }); 23 | 24 | var sound = context.createInterface("sound", { 25 | play: playSound, 26 | stop: stopSound 27 | }); 28 | 29 | var ifaces = [audio, ambience, music, sound]; 30 | 31 | function init() { 32 | 33 | var getModule = context.channel("getModule").call; 34 | 35 | Howl = getModule("howler").Howl; 36 | 37 | ifaces.forEach(context.connectInterface); 38 | 39 | vars = context.getInterface("vars", ["get", "set", "remove"]); 40 | settings = context.getInterface("settings", ["get"]); 41 | 42 | context.on("run_node", onRunNode); 43 | context.on("vars_resume", onResume); 44 | context.on("clear_state", audio.stop); 45 | context.on("update_setting", onUpdateSetting); 46 | } 47 | 48 | function destroy() { 49 | 50 | context.removeListener("run_node", onRunNode); 51 | context.removeListener("vars_resume", onResume); 52 | context.removeListener("clear_state", audio.stop); 53 | context.removeListener("update_setting", onUpdateSetting); 54 | 55 | ifaces.forEach(context.disconnectInterface); 56 | 57 | vars = null; 58 | audio = null; 59 | music = null; 60 | ambience = null; 61 | settings = null; 62 | currentAmbience = null; 63 | currentMusic = null; 64 | currentSound = null; 65 | } 66 | 67 | function onUpdateSetting(name) { 68 | if (name === "soundVolume" && currentSound) { 69 | currentSound.volume(settings.get("soundVolume") / 100); 70 | } 71 | else if (name === "ambienceVolume" && currentAmbience) { 72 | currentAmbience.volume(settings.get("ambienceVolume") / 100); 73 | } 74 | else if (name === "musicVolume" && currentMusic) { 75 | currentMusic.volume(settings.get("musicVolume") / 100); 76 | } 77 | } 78 | 79 | function onRunNode(node) { 80 | 81 | var data = node.data; 82 | 83 | if (data.audio === false) { 84 | audio.stop(); 85 | } 86 | 87 | if (data.sound) { 88 | sound.play(data.sound); 89 | } 90 | else { 91 | sound.stop(); 92 | } 93 | 94 | if (data.ambience) { 95 | ambience.play(data.ambience); 96 | } 97 | else if (data.ambience === false) { 98 | ambience.stop(); 99 | } 100 | 101 | if (data.music) { 102 | music.play(data.music); 103 | } 104 | else if (data.music === false) { 105 | music.stop(); 106 | } 107 | } 108 | 109 | function onResume() { 110 | 111 | if (vars.get("_currentSound")) { 112 | sound.play(audio.unserializePath(vars.get("_currentSound"))); 113 | } 114 | 115 | if (vars.get("_currentAmbience")) { 116 | ambience.play(audio.unserializePath(vars.get("_currentAmbience"))); 117 | } 118 | 119 | if (vars.get("_currentMusic")) { 120 | music.play(audio.unserializePath(vars.get("_currentMusic"))); 121 | } 122 | } 123 | 124 | function stopAudio() { 125 | sound.stop(); 126 | ambience.stop(); 127 | music.stop(); 128 | } 129 | 130 | function stopSound() { 131 | 132 | if (currentSound) { 133 | currentSound.unload(); 134 | } 135 | 136 | vars.remove("_currentSound"); 137 | currentSound = undefined; 138 | } 139 | 140 | function stopAmbience() { 141 | 142 | if (currentAmbience) { 143 | currentAmbience.unload(); 144 | } 145 | 146 | vars.remove("_currentAmbience"); 147 | currentAmbience = undefined; 148 | } 149 | 150 | function stopMusic() { 151 | 152 | if (currentMusic) { 153 | currentMusic.unload(); 154 | } 155 | 156 | vars.remove("_currentMusic"); 157 | currentMusic = undefined; 158 | } 159 | 160 | function playSound(path) { 161 | 162 | vars.set("_currentSound", audio.serializePath(path)); 163 | 164 | currentSound = audio.play(path, settings.get("soundVolume"), false, currentSound); 165 | } 166 | 167 | function playAmbience(path) { 168 | 169 | var serialized = audio.serializePath(path); 170 | 171 | if (currentAmbience && vars.get("_currentAmbience") === serialized) { 172 | return; 173 | } 174 | 175 | vars.set("_currentAmbience", serialized); 176 | 177 | currentAmbience = audio.play(path, settings.get("ambienceVolume"), true, currentAmbience); 178 | } 179 | 180 | function playMusic(path) { 181 | 182 | var serialized = audio.serializePath(path); 183 | 184 | if (currentMusic && vars.get("_currentMusic") === serialized) { 185 | return; 186 | } 187 | 188 | vars.set("_currentMusic", serialized); 189 | currentMusic = audio.play(path, settings.get("musicVolume"), true, currentMusic); 190 | } 191 | 192 | function playTrack(path, volume, loop, current) { 193 | 194 | var paths = audio.getPaths(path), track; 195 | 196 | track = new Howl({ 197 | src: paths, 198 | volume: volume / 100, 199 | loop: loop === true ? true : false 200 | }); 201 | 202 | if (current) { 203 | current.unload(); 204 | } 205 | 206 | track.play(); 207 | 208 | return track; 209 | } 210 | 211 | function getAudioPaths(path) { 212 | 213 | var paths = [], base; 214 | 215 | if (Array.isArray(path)) { 216 | 217 | path = path.slice(); 218 | base = path.shift(); 219 | 220 | path.forEach(function (type) { 221 | paths.push(base + "." + type); 222 | }); 223 | } 224 | else { 225 | paths.push(path); 226 | } 227 | 228 | return paths; 229 | } 230 | 231 | function serializeAudioPath(path) { 232 | return JSON.stringify(path); 233 | } 234 | 235 | function unserializeAudioPath(path) { 236 | return JSON.parse(path); 237 | } 238 | 239 | return { 240 | init: init, 241 | destroy: destroy 242 | }; 243 | } 244 | 245 | module.exports = { 246 | create: create 247 | }; 248 | -------------------------------------------------------------------------------- /resources/resources/templates/ui.html: -------------------------------------------------------------------------------- 1 |
2 | 87 |
88 |
89 | Tap anywhere to continue 90 |
91 |
92 |
93 | 94 | 95 | 96 |
97 |
98 | 99 | 100 | 101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |
144 |
-------------------------------------------------------------------------------- /src/runtime/browser-ui/cartridges.com.js: -------------------------------------------------------------------------------- 1 | 2 | var SCREEN_NAME = "cartridges"; 3 | var LINK_TYPE = "cartridge-link"; 4 | var IMAGE_TYPE = "cartridge-image"; 5 | var LINK_QUERY = "[data-type='" + LINK_TYPE + "']"; 6 | var IMAGE_QUERY = "[data-type='" + IMAGE_TYPE + "']"; 7 | 8 | var SOURCE_IMAGE_ID = "images/datacard.png"; 9 | 10 | function getImageElements() { 11 | return toArray(document.querySelectorAll(IMAGE_QUERY)); 12 | } 13 | 14 | function getLinkElements() { 15 | return toArray(document.querySelectorAll(LINK_QUERY)); 16 | } 17 | 18 | function toArray(collection) { 19 | return Array.prototype.slice.call(collection); 20 | } 21 | 22 | function create(context) { 23 | 24 | var interpreter, dropZone, confirm, sourceImage, resources, logger, cartridge, confirmation; 25 | 26 | var cartridges = context.createInterface("cartridges", { 27 | getSourceImage: getCartridgeSourceImage, 28 | showDropZone: showDropZone, 29 | hideDropZone: hideDropZone, 30 | generate: generateCartridge, 31 | getImageElements: getImageElements, 32 | getLinkElements: getLinkElements, 33 | readDatacard: readDatacard, 34 | readFile: readFile 35 | }); 36 | 37 | function init() { 38 | 39 | var getModule = context.channel("getModule"); 40 | 41 | cartridge = getModule("png-cartridge"); 42 | 43 | context.connectInterface(cartridges); 44 | 45 | confirmation = context.getInterface("uiConfirmations", ["create"]); 46 | interpreter = context.getInterface("interpreter", ["getState", "updateState"]); 47 | resources = context.getInterface("resources", ["get", "has"]); 48 | logger = context.getInterface("logger", ["log", "error"]); 49 | 50 | confirm = confirmation.create(); 51 | dropZone = document.createElement("div"); 52 | sourceImage = cartridges.getSourceImage(); 53 | 54 | dropZone.setAttribute("class", "global-drop-zone"); 55 | document.body.appendChild(dropZone); 56 | 57 | window.addEventListener("dragenter", cartridges.showDropZone); 58 | dropZone.addEventListener("dragenter", handleDragOver); 59 | dropZone.addEventListener("dragover", handleDragOver); 60 | dropZone.addEventListener("dragleave", cartridges.hideDropZone); 61 | dropZone.addEventListener("drop", handleDrop); 62 | 63 | context.on("showScreen", onShowScreen); 64 | context.on("cartridge_file_change", onFileChange); 65 | 66 | } 67 | 68 | function destroy() { 69 | 70 | context.disconnectInterface(cartridges); 71 | 72 | window.removeEventListener("dragenter", cartridges.showDropZone); 73 | context.removeListener("showScreen", onShowScreen); 74 | context.removeListener("cartridge_file_change", onFileChange); 75 | 76 | logger = null; 77 | resources = null; 78 | interpreter = null; 79 | confirm = null; 80 | dropZone = null; 81 | sourceImage = null; 82 | 83 | } 84 | 85 | function runScreenScript() { 86 | 87 | var file = document.querySelector(".file"); 88 | var uploadContainer = document.querySelector(".upload-container"); 89 | var downloadLink = document.querySelector(".download-link"); 90 | 91 | function onClick(event) { 92 | event.stopPropagation(); 93 | } 94 | 95 | function onFileChange(event) { 96 | context.publish("cartridge_file_change", event); 97 | } 98 | 99 | uploadContainer.addEventListener("click", onClick); 100 | downloadLink.addEventListener("click", onClick); 101 | file.addEventListener("change", onFileChange); 102 | 103 | } 104 | 105 | function onShowScreen(screen) { 106 | if (screen === SCREEN_NAME) { 107 | runScreenScript(); 108 | cartridges.generate(); 109 | } 110 | } 111 | 112 | function onFileChange(event) { 113 | 114 | var file = event.target.files[0]; 115 | 116 | event.preventDefault(); 117 | 118 | cartridges.readFile(file); 119 | } 120 | 121 | function generateCartridge() { 122 | 123 | var cart = cartridge.save(interpreter.getState(), sourceImage); 124 | 125 | cartridges.getImageElements().forEach(function (image) { 126 | image.src = cart.src; 127 | }); 128 | 129 | cartridges.getLinkElements().forEach(function (link) { 130 | link.href = cart.src; 131 | }); 132 | } 133 | 134 | function handleDrop(event) { 135 | 136 | var file = event.dataTransfer.files[0]; 137 | 138 | event.preventDefault(); 139 | cartridges.hideDropZone(); 140 | cartridges.readFile(file); 141 | } 142 | 143 | function readFile(file) { 144 | if (file && (/image\/png/).test(file.type)) { 145 | cartridges.readDatacard(file); 146 | } 147 | } 148 | 149 | function handleDragOver(event) { 150 | event.dataTransfer.dropEffect = "copy"; 151 | event.preventDefault(); 152 | } 153 | 154 | function showDropZone() { 155 | dropZone.style.visibility = "visible"; 156 | dropZone.style.opacity = 1; 157 | } 158 | 159 | function hideDropZone() { 160 | dropZone.style.visibility = "hidden"; 161 | dropZone.style.opacity = 0; 162 | } 163 | 164 | function readDatacard(file) { 165 | 166 | var reader = new FileReader(); 167 | 168 | reader.onload = function (event) { 169 | 170 | var data; 171 | var image = new Image(); 172 | 173 | document.body.appendChild(image); 174 | 175 | image.src = event.target.result; 176 | 177 | setTimeout(function () { 178 | 179 | try { 180 | data = cartridge.load(image); 181 | } 182 | catch (error) { 183 | document.body.removeChild(image); 184 | logger.error(error); 185 | return; 186 | } 187 | 188 | document.body.removeChild(image); 189 | 190 | confirm("Load state from datacard and discard progress?", function (yes) { 191 | if (yes) { 192 | interpreter.updateState(data); 193 | } 194 | }); 195 | 196 | }, 100); 197 | }; 198 | 199 | reader.readAsDataURL(file); 200 | } 201 | 202 | function getCartridgeSourceImage() { 203 | 204 | var uri = resources.get("images")[SOURCE_IMAGE_ID]; 205 | var image = new Image(); 206 | 207 | image.src = uri; 208 | image.style.opacity = "0"; 209 | 210 | document.body.appendChild(image); 211 | 212 | return image; 213 | } 214 | 215 | return { 216 | init: init, 217 | destroy: destroy 218 | }; 219 | 220 | } 221 | 222 | module.exports = { 223 | name: "cartridges", 224 | version: "2.0.0", 225 | application: "toothrot", 226 | applicationVersion: "2.x", 227 | applicationSteps: ["run"], 228 | environments: ["browser"], 229 | create: create 230 | }; 231 | -------------------------------------------------------------------------------- /src/validator.com.js: -------------------------------------------------------------------------------- 1 | 2 | function create(context) { 3 | 4 | var createError; 5 | 6 | var api = context.createInterface("validator", { 7 | createValidator: createValidator 8 | }); 9 | 10 | function init() { 11 | 12 | createError = context.channel("toothrotErrors/createError").call; 13 | 14 | context.connectInterface(api); 15 | } 16 | 17 | function destroy() { 18 | context.disconnectInterface(api); 19 | } 20 | 21 | function createValidator(handleError) { 22 | 23 | function validateNodes(nodes) { 24 | Object.keys(nodes).forEach(function (key) { 25 | validateNode(nodes[key], key, nodes); 26 | }); 27 | } 28 | 29 | function validateAst(ast) { 30 | 31 | if (!ast.meta.title) { 32 | handleError(createError({id: "NO_TITLE"})); 33 | } 34 | 35 | if (!ast.nodes.start) { 36 | handleError(createError({id: "NO_START_NODE"})); 37 | } 38 | 39 | validateHierarchy(ast.head.hierarchy); 40 | validateNodes(ast.nodes); 41 | } 42 | 43 | function validateHierarchy(hierarchy) { 44 | 45 | try { 46 | Object.keys(hierarchy).forEach(function (tag) { 47 | resolveHierarchy(tag, hierarchy); 48 | }); 49 | } 50 | catch (tag) { 51 | handleError(createError({ 52 | id: "CIRCULAR_HIERARCHY", 53 | tag: tag 54 | })); 55 | } 56 | } 57 | 58 | function resolveHierarchy(tag, hierarchy, resolving) { 59 | 60 | var tags = []; 61 | var ancestors = hierarchy[tag]; 62 | 63 | resolving = resolving || []; 64 | 65 | resolving.push(tag); 66 | 67 | ancestors.forEach(function (ancestor) { 68 | 69 | if (resolving.indexOf(ancestor) >= 0) { 70 | throw tag; 71 | } 72 | 73 | resolveHierarchy(ancestor, hierarchy, resolving).forEach(function (otherTag) { 74 | tags.push(otherTag); 75 | }); 76 | 77 | }); 78 | 79 | resolving.splice(resolving.indexOf(tag), 1); 80 | 81 | return tags; 82 | } 83 | 84 | function validateNode(node, name, nodes) { 85 | 86 | if (node.next && !nodes[node.next]) { 87 | handleError(createError({ 88 | id: "UNKNOWN_NEXT_NODE", 89 | next: node.next, 90 | nodeLine: node.line, 91 | nodeFile: node.file, 92 | nodeId: node.id 93 | })); 94 | } 95 | 96 | if (node.data.autonextTarget && !nodes[node.data.autonextTarget]) { 97 | handleError(createError({ 98 | id: "UNKNOWN_AUTONEXT_TARGET", 99 | target: node.data.autonextTarget, 100 | nodeLine: node.line, 101 | nodeFile: node.file, 102 | nodeId: node.id 103 | })); 104 | } 105 | 106 | if (node.next && node.returnToLast) { 107 | handleError(createError({ 108 | id: "CONFLICT_NEXT_RETURN", 109 | nodeId: node.id, 110 | nodeFile: node.file, 111 | nodeLine: node.line 112 | })); 113 | } 114 | 115 | if ( 116 | node.data.autonext && 117 | !node.returnToLast && 118 | !node.next && 119 | !node.data.autonextTarget 120 | ) { 121 | handleError(createError({ 122 | id: "NO_AUTONEXT_TARGET", 123 | nodeId: node.id, 124 | nodeFile: node.file, 125 | nodeLine: node.line 126 | })); 127 | } 128 | 129 | validateOptions(node, nodes); 130 | validateLinks(node, nodes); 131 | } 132 | 133 | function validateOptions(node, nodes) { 134 | if (Array.isArray(node.options)) { 135 | node.options.forEach(function (option) { 136 | validateOption(option, node, nodes); 137 | }); 138 | } 139 | } 140 | 141 | function validateOption(option, node, nodes) { 142 | 143 | if (!option.target && !option.value) { 144 | handleError(createError({ 145 | id: "OPTION_WITHOUT_TARGET_OR_VALUE", 146 | nodeId: node.id, 147 | nodeLine: node.line, 148 | nodeFile: node.file, 149 | optionFile: option.file, 150 | optionLine: option.line 151 | })); 152 | } 153 | 154 | if (option.target && !nodes[option.target]) { 155 | handleError(createError({ 156 | id: "UNKNOWN_OPTION_TARGET", 157 | nodeId: node.id, 158 | target: option.target, 159 | label: option.label, 160 | nodeLine: node.line, 161 | nodeFile: node.file, 162 | optionFile: option.file, 163 | optionLine: option.line 164 | })); 165 | } 166 | } 167 | 168 | function validateLinks(node, nodes) { 169 | node.links.forEach(function (link) { 170 | validateLink(link, node, nodes); 171 | }); 172 | } 173 | 174 | function validateLink(link, node, nodes) { 175 | if (link.type === "direct_link") { 176 | validateDirectLink(link, node, nodes); 177 | } 178 | } 179 | 180 | function validateDirectLink(link, node, nodes) { 181 | 182 | if (!link.target) { 183 | handleError(createError({ 184 | id: "NO_LINK_TARGET", 185 | nodeId: node.id, 186 | nodeLine: node.line, 187 | file: node.file, 188 | linkLine: link.line, 189 | label: link.label 190 | })); 191 | } 192 | else if (!(link.target in nodes)) { 193 | handleError(createError({ 194 | id: "UNKNOWN_LINK_TARGET", 195 | nodeId: node.id, 196 | nodeLine: node.line, 197 | file: node.file, 198 | linkLine: link.line, 199 | target: link.target, 200 | label: link.label 201 | })); 202 | } 203 | } 204 | 205 | return { 206 | validate: validateAst, 207 | validateNodes: validateNodes, 208 | validateNode: validateNode 209 | }; 210 | } 211 | 212 | return { 213 | init: init, 214 | destroy: destroy 215 | }; 216 | } 217 | 218 | module.exports = { 219 | create: create 220 | }; 221 | -------------------------------------------------------------------------------- /src/builder.com.js: -------------------------------------------------------------------------------- 1 | /* global require, Buffer, process */ 2 | 3 | function create(context) { 4 | 5 | var logger, packer, joinPath, normalize, gatherer, fsHelper, scriptExporter, format; 6 | 7 | var api = context.createInterface("builder", { 8 | build: build, 9 | reportErrors: reportErrors 10 | }); 11 | 12 | function init() { 13 | 14 | var getModule = context.channel("getModule"); 15 | var path = getModule("path"); 16 | 17 | joinPath = path.join; 18 | normalize = path.normalize; 19 | 20 | format = getModule("vrep").create("{{$", "}}"); 21 | logger = context.getInterface("logger", ["log", "error", "info", "success"]); 22 | packer = context.getInterface("packer", ["pack"]); 23 | gatherer = context.getInterface("toothrotGatherer", ["renderTemplate"]); 24 | scriptExporter = context.getInterface("scriptExporter", ["render"]); 25 | 26 | fsHelper = context.getInterface("fileSystem", [ 27 | "copyAll", 28 | "removeRecursive", 29 | "readDirRecursive" 30 | ]); 31 | 32 | context.connectInterface(api); 33 | } 34 | 35 | function destroy() { 36 | 37 | logger = null; 38 | packer = null; 39 | gatherer = null; 40 | scriptExporter = null; 41 | 42 | context.disconnectInterface(api); 43 | } 44 | 45 | function gathererDependencyInjector(fs) { 46 | 47 | var getModule = context.channel("getModule"); 48 | 49 | var modules = { 50 | fs: fs, 51 | assert: getModule("assert"), 52 | semver: getModule("semver"), 53 | minimatch: getModule("minimatch") 54 | }; 55 | 56 | return function (host) { 57 | host.decorate("getModule", function (fn) { 58 | return function (name) { 59 | 60 | var result = fn(name); 61 | 62 | if (result) { 63 | return result; 64 | } 65 | 66 | return name in modules ? modules[name] : getModule(name); 67 | }; 68 | }); 69 | }; 70 | } 71 | 72 | function gatherComponents(fs, dir) { 73 | 74 | var componentFile, template, componentTemplate; 75 | var getResource = context.channel("getResource"); 76 | 77 | logger.info("Gathering components in `" + dir + "`..."); 78 | 79 | template = getResource("toothrotResourcesTemplate"); 80 | componentTemplate = getResource("toothrotComponentTemplate"); 81 | 82 | componentFile = gatherer.renderTemplate(fs, template, componentTemplate, { 83 | paths: [dir], 84 | prepareHost: gathererDependencyInjector(fs) 85 | }); 86 | 87 | logger.success("Component gathering completed."); 88 | 89 | return componentFile; 90 | } 91 | 92 | function createBootstrapFiles(inputFs, dir, outputFs, outputPath) { 93 | 94 | var runtimePath = joinPath(dir, "toothrot.js"); 95 | var componentFileContent = gatherComponents(inputFs, dir); 96 | var appDestFilePath = joinPath(outputPath, "/toothrot.js"); 97 | var componentFilePath = joinPath(outputPath, "/components.js"); 98 | 99 | logger.info( 100 | "Bundling browser components for project in `" + dir + "`..." 101 | ); 102 | 103 | logger.log("Creating bootstrap file for browser in `" + outputPath + "`..."); 104 | 105 | if (!outputFs.existsSync(outputPath)) { 106 | logger.log("Creating temporary folder `" + outputPath + "`..."); 107 | outputFs.mkdirSync(outputPath); 108 | } 109 | 110 | outputFs.writeFileSync(appDestFilePath, inputFs.readFileSync(runtimePath)); 111 | outputFs.writeFileSync(componentFilePath, componentFileContent); 112 | 113 | logger.success("Browser component bundle written successfully."); 114 | 115 | } 116 | 117 | function build(inputFs, dir, outputFs, outputDir, then) { 118 | 119 | var rawResources, resources, indexContent, story, storyErrors; 120 | 121 | var base = normalize((dir || process.cwd()) + "/"); 122 | var buildDir = normalize(outputDir || (base + "build/")); 123 | var browserDir = joinPath(buildDir, "/browser/"); 124 | var scriptFilePath = joinPath(browserDir, "/scripts.js"); 125 | var filesDir = joinPath(base, "/files/"); 126 | var projectFile = joinPath(base, "/project.json"); 127 | var project = JSON.parse("" + inputFs.readFileSync(projectFile)); 128 | 129 | context.publish("builder/build.before", { 130 | inputFs: inputFs, 131 | inputDir: dir, 132 | outputFs: outputFs, 133 | outputDir: outputDir 134 | }); 135 | 136 | then = then || function () {}; 137 | 138 | if (!outputFs.existsSync(buildDir)) { 139 | outputFs.mkdirSync(buildDir); 140 | } 141 | 142 | if (outputFs.existsSync(browserDir)) { 143 | fsHelper.removeRecursive(outputFs, browserDir); 144 | } 145 | 146 | outputFs.mkdirSync(browserDir); 147 | 148 | createBootstrapFiles(inputFs, base, outputFs, browserDir); 149 | 150 | logger.info("Copying files from `" + filesDir + "` to `" + browserDir + "`..."); 151 | 152 | fsHelper.copyAll(inputFs, filesDir, outputFs, browserDir); 153 | 154 | logger.success("Files copied to `" + browserDir + "`."); 155 | 156 | packer.pack(inputFs, base, function (error, storyPack) { 157 | storyErrors = error; 158 | rawResources = storyPack; 159 | }); 160 | 161 | if (storyErrors) { 162 | 163 | // @ts-ignore 164 | if (storyErrors.isToothrotError) { 165 | then(storyErrors); 166 | // @ts-ignore 167 | logger.error(storyErrors.toothrotMessage); 168 | return; 169 | } 170 | 171 | if (Array.isArray(storyErrors)) { 172 | reportErrors(storyErrors); 173 | then(storyErrors); 174 | return; 175 | } 176 | 177 | throw storyErrors; 178 | } 179 | 180 | story = JSON.parse(rawResources).story; 181 | project.name = story.meta.title || project.name; 182 | resources = new Buffer(encodeURIComponent(rawResources)).toString("base64"); 183 | 184 | indexContent = format( 185 | context.channel("getResource")("toothrotResourceFileTemplate"), 186 | { 187 | resources: resources 188 | } 189 | ); 190 | 191 | outputFs.writeFileSync(joinPath(browserDir, "resources.js"), indexContent); 192 | outputFs.writeFileSync(scriptFilePath, scriptExporter.render(story)); 193 | outputFs.writeFileSync(projectFile, JSON.stringify(project, null, 4)); 194 | 195 | logger.success("Toothrot project built successfully in: " + browserDir); 196 | 197 | context.publish("builder/build.after", { 198 | inputFs: inputFs, 199 | inputDir: dir, 200 | outputFs: outputFs, 201 | outputDir: outputDir, 202 | browserDir: browserDir 203 | }); 204 | 205 | } 206 | 207 | function reportErrors(errors) { 208 | errors.forEach(function (error) { 209 | logger.error(error.toothrotMessage || error.message); 210 | }); 211 | } 212 | 213 | return { 214 | init: init, 215 | destroy: destroy 216 | }; 217 | } 218 | 219 | module.exports = { 220 | create: create 221 | }; 222 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "no-console": ["error", {"allow": ["warn", "error"]}], 9 | "accessor-pairs": "error", 10 | "array-bracket-spacing": [ 11 | "error", 12 | "never" 13 | ], 14 | "array-callback-return": "error", 15 | "arrow-body-style": "error", 16 | "arrow-parens": "error", 17 | "arrow-spacing": "error", 18 | "block-scoped-var": "error", 19 | "block-spacing": "error", 20 | "brace-style": [ 21 | "error", 22 | "stroustrup" 23 | ], 24 | "callback-return": "off", 25 | "camelcase": "error", 26 | "comma-dangle": "error", 27 | "comma-spacing": [ 28 | "error", 29 | { 30 | "after": true, 31 | "before": false 32 | } 33 | ], 34 | "comma-style": [ 35 | "error", 36 | "last" 37 | ], 38 | "complexity": "error", 39 | "computed-property-spacing": [ 40 | "error", 41 | "never" 42 | ], 43 | "consistent-return": "off", 44 | "consistent-this": ["error", "self"], 45 | "curly": "error", 46 | "default-case": "error", 47 | "dot-location": [ 48 | "error", 49 | "object" 50 | ], 51 | "dot-notation": [ 52 | "error", 53 | { 54 | "allowKeywords": true 55 | } 56 | ], 57 | "eol-last": "error", 58 | "eqeqeq": "error", 59 | "func-names": [ 60 | "error", 61 | "never" 62 | ], 63 | "func-style": "off", 64 | "generator-star-spacing": "error", 65 | "global-require": "off", 66 | "guard-for-in": "off", 67 | "handle-callback-err": "error", 68 | "id-blacklist": "error", 69 | "id-length": "off", 70 | "id-match": "error", 71 | "indent": ["error", 4], 72 | "init-declarations": "off", 73 | "jsx-quotes": "error", 74 | "key-spacing": "error", 75 | "keyword-spacing": "error", 76 | "linebreak-style": [ 77 | "error", 78 | "unix" 79 | ], 80 | "lines-around-comment": "error", 81 | "max-depth": "error", 82 | "max-len": ["error", 100], 83 | "max-lines": "off", 84 | "max-nested-callbacks": "error", 85 | "max-params": "off", 86 | "max-statements": "off", 87 | "max-statements-per-line": "error", 88 | "multiline-ternary": "off", 89 | "new-cap": "error", 90 | "new-parens": "error", 91 | "newline-after-var": "off", 92 | "newline-before-return": "off", 93 | "newline-per-chained-call": "off", 94 | "no-alert": "off", 95 | "no-array-constructor": "error", 96 | "no-bitwise": "error", 97 | "no-caller": "error", 98 | "no-catch-shadow": "off", 99 | "no-confusing-arrow": "error", 100 | "no-continue": "error", 101 | "no-div-regex": "error", 102 | "no-duplicate-imports": "error", 103 | "no-else-return": "off", 104 | "no-empty-function": "off", 105 | "no-eq-null": "error", 106 | "no-eval": "error", 107 | "no-extend-native": "error", 108 | "no-extra-bind": "error", 109 | "no-extra-label": "error", 110 | "no-extra-parens": "off", 111 | "no-floating-decimal": "error", 112 | "no-implicit-globals": "error", 113 | "no-implied-eval": "error", 114 | "no-inline-comments": "off", 115 | "no-inner-declarations": [ 116 | "error", 117 | "functions" 118 | ], 119 | "no-invalid-this": "error", 120 | "no-iterator": "error", 121 | "no-label-var": "error", 122 | "no-labels": "error", 123 | "no-lone-blocks": "error", 124 | "no-lonely-if": "off", 125 | "no-loop-func": "error", 126 | "no-magic-numbers": "off", 127 | "no-mixed-operators": "error", 128 | "no-mixed-requires": "error", 129 | "no-multi-spaces": "error", 130 | "no-multi-str": "error", 131 | "no-multiple-empty-lines": "error", 132 | "no-negated-condition": "off", 133 | "no-nested-ternary": "error", 134 | "no-new": "error", 135 | "no-new-func": "error", 136 | "no-new-object": "error", 137 | "no-new-require": "error", 138 | "no-new-wrappers": "error", 139 | "no-octal-escape": "error", 140 | "no-param-reassign": "off", 141 | "no-path-concat": "off", 142 | "no-plusplus": "error", 143 | "no-process-env": "error", 144 | "no-process-exit": "error", 145 | "no-proto": "error", 146 | "no-prototype-builtins": "error", 147 | "no-restricted-globals": "error", 148 | "no-restricted-imports": "error", 149 | "no-restricted-modules": "error", 150 | "no-restricted-syntax": "error", 151 | "no-return-assign": "off", 152 | "no-script-url": "error", 153 | "no-self-compare": "error", 154 | "no-sequences": "error", 155 | "no-shadow": "off", 156 | "no-shadow-restricted-names": "error", 157 | "no-spaced-func": "error", 158 | "no-sync": "off", 159 | "no-tabs": "error", 160 | "no-ternary": "off", 161 | "no-throw-literal": "error", 162 | "no-trailing-spaces": "off", 163 | "no-undef-init": "error", 164 | "no-undefined": "off", 165 | "no-underscore-dangle": "off", 166 | "no-unmodified-loop-condition": "error", 167 | "no-unneeded-ternary": "off", 168 | "no-unused-expressions": "error", 169 | "no-use-before-define": "off", 170 | "no-useless-call": "error", 171 | "no-useless-computed-key": "error", 172 | "no-useless-concat": "error", 173 | "no-useless-constructor": "error", 174 | "no-useless-escape": "off", 175 | "no-useless-rename": "error", 176 | "no-var": "off", 177 | "no-void": "error", 178 | "no-warning-comments": "error", 179 | "no-whitespace-before-property": "error", 180 | "no-with": "error", 181 | "object-curly-newline": "off", 182 | "object-curly-spacing": [ 183 | "error", 184 | "never" 185 | ], 186 | "object-property-newline": [ 187 | "error", 188 | { 189 | "allowMultiplePropertiesPerLine": true 190 | } 191 | ], 192 | "object-shorthand": "off", 193 | "one-var": "off", 194 | "one-var-declaration-per-line": "off", 195 | "operator-assignment": [ 196 | "error", 197 | "always" 198 | ], 199 | "operator-linebreak": [ 200 | "error", 201 | "after" 202 | ], 203 | "padded-blocks": "off", 204 | "prefer-arrow-callback": "off", 205 | "prefer-const": "error", 206 | "prefer-reflect": "off", 207 | "prefer-rest-params": "off", 208 | "prefer-spread": "off", 209 | "prefer-template": "off", 210 | "quote-props": "off", 211 | "quotes": "off", 212 | "radix": "error", 213 | "require-jsdoc": "off", 214 | "rest-spread-spacing": "error", 215 | "semi": "error", 216 | "semi-spacing": [ 217 | "error", 218 | { 219 | "after": true, 220 | "before": false 221 | } 222 | ], 223 | "sort-imports": "error", 224 | "sort-vars": "off", 225 | "space-before-blocks": "error", 226 | "space-before-function-paren": ["error", { 227 | "anonymous": "always", 228 | "named": "never", 229 | "asyncArrow": "always" 230 | }], 231 | "space-in-parens": [ 232 | "error", 233 | "never" 234 | ], 235 | "space-infix-ops": "off", 236 | "space-unary-ops": "error", 237 | "spaced-comment": [ 238 | "error", 239 | "always" 240 | ], 241 | "strict": [ 242 | "error", 243 | "never" 244 | ], 245 | "template-curly-spacing": "error", 246 | "unicode-bom": [ 247 | "error", 248 | "never" 249 | ], 250 | "valid-jsdoc": "error", 251 | "vars-on-top": "off", 252 | "wrap-iife": "error", 253 | "wrap-regex": "error", 254 | "yield-star-spacing": "error", 255 | "yoda": [ 256 | "error", 257 | "never" 258 | ] 259 | } 260 | } -------------------------------------------------------------------------------- /src/runtime/nodes.com.js: -------------------------------------------------------------------------------- 1 | 2 | function contains(array, thing) { 3 | return (array.indexOf(thing) >= 0); 4 | } 5 | 6 | function createApi(id, data, nodes) { 7 | 8 | var api = { 9 | 10 | id: id, 11 | 12 | isA: isA, 13 | isntA: isntA, 14 | is: is, 15 | isnt: isnt, 16 | isIn: isIn, 17 | isntIn: isntIn, 18 | isSneaky: isSneaky, 19 | isntSneaky: isntSneaky, 20 | isEmpty: isEmpty, 21 | isntEmpty: isntEmpty, 22 | be: be, 23 | beSneaky: beSneaky, 24 | dontBe: dontBe, 25 | dontBeSneaky: dontBeSneaky, 26 | moveTo: moveTo, 27 | insert: insert, 28 | contains: containsNode, 29 | children: children, 30 | doesntContain: doesntContain, 31 | get: get, 32 | set: set, 33 | has: has, 34 | prop: prop, 35 | raw: raw 36 | }; 37 | 38 | function get(key) { 39 | return data[key]; 40 | } 41 | 42 | function has(key) { 43 | return (key in data); 44 | } 45 | 46 | function set(key, value) { 47 | data[key] = value; 48 | return api; 49 | } 50 | 51 | function isA(tag) { 52 | return contains(data.tags, tag); 53 | } 54 | 55 | function isntA(tag) { 56 | return !isA(tag); 57 | } 58 | 59 | function is(flag) { 60 | return contains(data.flags, flag); 61 | } 62 | 63 | function isnt(flag) { 64 | return !is(flag); 65 | } 66 | 67 | function isEmpty() { 68 | return data.contains.length < 1; 69 | } 70 | 71 | function isntEmpty() { 72 | return !isEmpty(); 73 | } 74 | 75 | function containsNode(id) { 76 | return contains(data.contains, id); 77 | } 78 | 79 | function doesntContain(id) { 80 | return !containsNode(id); 81 | } 82 | 83 | function isIn(id) { 84 | 85 | if (!nodes.has(id)) { 86 | return false; 87 | } 88 | 89 | return nodes.get(id).contains(api.id); 90 | } 91 | 92 | function isntIn(id) { 93 | return !isIn(id); 94 | } 95 | 96 | function isSneaky() { 97 | return is("sneaky"); 98 | } 99 | 100 | function isntSneaky() { 101 | return !isSneaky(); 102 | } 103 | 104 | function be(flag) { 105 | 106 | if (!is(flag)) { 107 | data.flags.push(flag); 108 | } 109 | 110 | return api; 111 | } 112 | 113 | function beSneaky() { 114 | return be("sneaky"); 115 | } 116 | 117 | function dontBe(flag) { 118 | 119 | if (is(flag)) { 120 | data.flags = data.flags.filter(function (nodeFlag) { 121 | return nodeFlag !== flag; 122 | }); 123 | } 124 | 125 | return api; 126 | } 127 | 128 | function dontBeSneaky() { 129 | return dontBe("sneaky"); 130 | } 131 | 132 | function moveTo(otherId) { 133 | 134 | var otherNode; 135 | 136 | if (!nodes.has(otherId)) { 137 | throw new Error( 138 | "Cannot move node '" + id + "' to node '" + otherId + "': " + 139 | "No such node ID!" 140 | ); 141 | } 142 | 143 | otherNode = nodes.get(otherId); 144 | 145 | data.wasIn.forEach(function (itemId) { 146 | 147 | var item = nodes.get(itemId); 148 | 149 | item.set("contains", item.get("contains").filter(function (child) { 150 | return child !== id; 151 | })); 152 | 153 | }); 154 | 155 | otherNode.insert(id); 156 | 157 | return api; 158 | } 159 | 160 | function insert(otherId) { 161 | 162 | var otherNode; 163 | 164 | if (!nodes.has(otherId)) { 165 | console.warn("No such node ID: " + otherId); 166 | return api; 167 | } 168 | 169 | if (!contains(otherId)) { 170 | 171 | data.contains.push(otherId); 172 | 173 | otherNode = nodes.get(otherId); 174 | 175 | if (otherNode.get("wasIn").indexOf(id) < 0) { 176 | otherNode.get("wasIn").push(id); 177 | } 178 | } 179 | 180 | return api; 181 | } 182 | 183 | function children() { 184 | return data.contains.slice(); 185 | } 186 | 187 | function prop(name, value) { 188 | 189 | if (arguments.length > 1) { 190 | data[name] = value; 191 | } 192 | 193 | return data[name]; 194 | } 195 | 196 | function raw() { 197 | return data; 198 | } 199 | 200 | return api; 201 | } 202 | 203 | function createKey(id) { 204 | return "__objdata_" + id; 205 | } 206 | 207 | function create(context) { 208 | 209 | var story, vars, env, clone; 210 | 211 | var api = context.createInterface("nodes", { 212 | get: get, 213 | has: has, 214 | hasData: hasData, 215 | getData: getData, 216 | resolveTagHierarchy: resolveTagHierarchy 217 | }); 218 | 219 | function init() { 220 | 221 | var getModule = context.channel("getModule").call; 222 | 223 | clone = getModule("clone"); 224 | 225 | context.connectInterface(api); 226 | 227 | env = context.getInterface("env", ["set"]); 228 | vars = context.getInterface("vars", ["get", "set"]); 229 | story = context.getInterface("story", ["getHierarchy", "getNode", "hasNode"]); 230 | 231 | env.set("last", function (tag) { 232 | 233 | var lastNodes = vars.get("_lastNodes") || {}; 234 | 235 | return lastNodes[tag]; 236 | }); 237 | 238 | context.on("before_run_node", onBeforeRunNode); 239 | } 240 | 241 | function destroy() { 242 | 243 | context.removeListener("before_run_node", onBeforeRunNode); 244 | context.disconnectInterface(api); 245 | 246 | vars = null; 247 | story = null; 248 | } 249 | 250 | function onBeforeRunNode(node) { 251 | 252 | var lastNodes = vars.get("_lastNodes") || {}; 253 | 254 | api.getData(node.id).tags.forEach(function (originalTag) { 255 | 256 | var allTags = api.resolveTagHierarchy(originalTag); 257 | 258 | allTags.push(originalTag); 259 | 260 | allTags.forEach(function (tag) { 261 | lastNodes[tag] = node.id; 262 | }); 263 | }); 264 | 265 | vars.set("_lastNodes", lastNodes); 266 | } 267 | 268 | function resolveTagHierarchy(tag) { 269 | 270 | var tags = []; 271 | var hierarchy = story.getHierarchy(); 272 | var ancestors = hierarchy[tag] || []; 273 | 274 | ancestors.forEach(function (ancestor) { 275 | 276 | if (tags.indexOf(ancestor) >= 0) { 277 | return; 278 | } 279 | 280 | tags.push(ancestor); 281 | 282 | resolveTagHierarchy(ancestor).forEach(function (otherTag) { 283 | tags.push(otherTag); 284 | }); 285 | }); 286 | 287 | return tags; 288 | } 289 | 290 | function get(id) { 291 | 292 | var data; 293 | 294 | if (!api.has(id)) { 295 | throw new Error("No such node id: " + id); 296 | } 297 | 298 | data = api.getData(id); 299 | 300 | return createApi(id, data, context.getInterface("nodes", ["get", "has"])); 301 | } 302 | 303 | function has(id) { 304 | return api.hasData(id) || story.hasNode(id); 305 | } 306 | 307 | function hasData(id) { 308 | return !!vars.get(createKey(id)); 309 | } 310 | 311 | function getData(id) { 312 | 313 | var key = createKey(id); 314 | var data = vars.get(key); 315 | 316 | if (!data) { 317 | data = clone(story.getNode(id).data); 318 | vars.set(key, data); 319 | } 320 | 321 | return data; 322 | } 323 | 324 | return { 325 | init: init, 326 | destroy: destroy 327 | }; 328 | } 329 | 330 | module.exports = { 331 | name: "nodes", 332 | version: "2.0.0", 333 | application: "toothrot", 334 | applicationVersion: "2.x", 335 | applicationSteps: ["run"], 336 | environments: ["any"], 337 | create: create 338 | }; 339 | --------------------------------------------------------------------------------