├── .gitattributes ├── .babelrc ├── .gitignore ├── scaffold ├── defines.json ├── script.js ├── template.pug └── style.less ├── .editorconfig ├── gulpfile.js ├── LICENSE ├── package.json ├── README.md └── lib └── gall.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /scaffold/defines.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "A Blotter Story" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /scaffold/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | This script file runs after Blotter's main bundle and can contain code 3 | to support custom UI and logic. 4 | 5 | The main story object from inkjs is present in window.story. 6 | */ 7 | 8 | window.blotterStart(); // Start up Blotter 9 | -------------------------------------------------------------------------------- /scaffold/template.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="utf-8") 5 | title !{ defines.title } 6 | script(id="storyscript" type="application/json") !{ story } 7 | style !{ css } 8 | body 9 | div#content-container 10 | div#story-stage 11 | div#choices 12 | script !{ blotter } 13 | script !{ script } 14 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var nsp = require('gulp-nsp'); 4 | var babel = require('gulp-babel'); 5 | var del = require('del'); 6 | 7 | // Initialize the babel transpiler so ES2015 files gets compiled 8 | // when they're loaded 9 | require('babel-register'); 10 | 11 | gulp.task('nsp', function (cb) { 12 | nsp({package: path.resolve('package.json')}, cb); 13 | }); 14 | 15 | gulp.task('watch', function () { 16 | gulp.watch(['lib/**/*.js', 'test/**'], ['test']); 17 | }); 18 | 19 | gulp.task('babel', ['clean'], function () { 20 | return gulp.src('lib/**/*.js') 21 | .pipe(babel()) 22 | .pipe(gulp.dest('dist')); 23 | }); 24 | 25 | gulp.task('clean', function () { 26 | return del('dist'); 27 | }); 28 | 29 | gulp.task('prepublish', ['nsp', 'babel']); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bruno Dias (http://segue.pw) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gall", 3 | "version": "0.3.1", 4 | "description": "A command-line tool for building Blotter projects.", 5 | "homepage": "", 6 | "author": { 7 | "name": "Bruno Dias", 8 | "email": "bruno.r.dias@gmail.com", 9 | "url": "http://segue.pw" 10 | }, 11 | "files": [ 12 | "dist", 13 | "scaffold" 14 | ], 15 | "bin": { 16 | "gall": "dist/gall.js" 17 | }, 18 | "keywords": [ 19 | "" 20 | ], 21 | "devDependencies": { 22 | "babel-core": "^6.25.0", 23 | "babel-eslint": "^7.2.3", 24 | "babel-preset-es2015": "^6.24.1", 25 | "babel-register": "^6.24.1", 26 | "del": "^3.0.0", 27 | "eslint": "^4.4.1", 28 | "eslint-config-xo-space": "^0.16.0", 29 | "eslint-plugin-babel": "^4.1.2", 30 | "gulp": "^3.9.1", 31 | "gulp-babel": "^7.0.0", 32 | "gulp-eslint": "^4.0.0", 33 | "gulp-line-ending-corrector": "^1.0.1", 34 | "gulp-nsp": "^2.4.2" 35 | }, 36 | "eslintConfig": { 37 | "extends": "xo-space", 38 | "env": { 39 | "mocha": true 40 | } 41 | }, 42 | "repository": "sequitur/gall", 43 | "scripts": { 44 | "prepublish": "gulp prepublish" 45 | }, 46 | "license": "MIT", 47 | "dependencies": { 48 | "boom": "^5.2.0", 49 | "chokidar": "^1.6.1", 50 | "colors": "^1.1.2", 51 | "commander": "^2.11.0", 52 | "fs-jetpack": "^0.9.2", 53 | "ink-blotter": "^0.5.0", 54 | "less": "^2.7.1", 55 | "pug": "^2.0.0-beta6" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gall 2 | 3 | Gall is a simple command-line tool for assembling Ink stories with [blotter](http://github.com/sequitur/blotter). 4 | 5 | ## Installation 6 | 7 | Make sure you have an up-to-date version of Node and npm installed in your system, then install Gall from github: 8 | 9 | ``` 10 | $ npm install -g gall 11 | ``` 12 | 13 | ## Usage 14 | 15 | In an empty directory, create a Gall project: 16 | 17 | ``` 18 | $ gall new 19 | ``` 20 | 21 | This will create a `sources/` directory there, containing the following files: 22 | 23 | - *defines.json*: Definitions that can be inserted into the template, including the story title. 24 | - *style.less*: A Less file that acts as the stylesheet for the project. 25 | - *template.pug*: A Pug template that acts as the template for the project. 26 | - *script.js*: A javascript script that is run after the main Blotter script, which can contain any custom functionality you want to hook into inkjs before starting up the main UI loop through Blotter. 27 | 28 | Gall will populate those files with working ones from a scaffold, which you can customize as you like. You will also have to provide a json file compiled from your Ink story through Inkle's inklecate compiler, and place it in `sources/story.ink.json`. 29 | 30 | With everything in place, `gall build` will build your story and output it as `out.html`. Stories generated by Gall are self-contained html files, for portability and performance; all of the css (automatically generated by Less), js (imported from the Blotter project build file), and Ink data are inlined into the file. 31 | -------------------------------------------------------------------------------- /scaffold/style.less: -------------------------------------------------------------------------------- 1 | /* Gall scaffold Less file */ 2 | 3 | /* We use Cormorant Garamond as our default font, but you can easily use 4 | whatever you like. */ 5 | @import 'https://fonts.googleapis.com/css?family=Cormorant+Garamond:400,400i'; 6 | 7 | body,html { 8 | font-family: 'Cormorant Garamond', serif; 9 | font-size: 22px; 10 | } 11 | 12 | /* Keyframe definitions for the fade in animation on paragraphs. */ 13 | @keyframes fadein { 14 | from { 15 | opacity: 0; 16 | } 17 | to { 18 | opacity: 1; 19 | } 20 | } 21 | 22 | /* !! Important 23 | Blotter relies on animations and transitions to time output actions. 24 | Consequently, you should absolutely *not* remove them entirely from the 25 | stylesheet; this will cause blotter to not work. Changing those animations 26 | and transitions is fine, just as long as they are there. 27 | 28 | Animation properties that are required by Blotter are marked with a 29 | comment. 30 | */ 31 | 32 | #content-container { 33 | height: 95vh; 34 | max-width: 40rem; 35 | margin: auto; 36 | #story-stage { 37 | height: calc(100% - 10rem); 38 | margin: 0; 39 | padding: 0; 40 | overflow-y: auto; 41 | p,h1,h2,h3,h4,h5,h6 { 42 | animation: 1s fadein; // REQUIRED 43 | } 44 | 45 | } 46 | 47 | #choices { 48 | height: 3rem; 49 | margin: 0; 50 | padding: 0; 51 | .choices { 52 | animation: .5s fadein; // REQUIRED 53 | display: flex; 54 | flex-flow: row wrap; 55 | margin: 0; 56 | padding: 0; 57 | 58 | li { 59 | background: #ddc; 60 | line-height: 1.25rem; 61 | display: block; 62 | list-style: none; 63 | margin-top: 0; 64 | margin-bottom: .1rem; 65 | margin-left: .1rem; 66 | margin-right: .1rem; 67 | font-size: 1rem; 68 | padding: .25rem; 69 | flex-grow: 1; 70 | text-align: center; 71 | cursor: pointer; 72 | 73 | &:hover { 74 | background: #ccd; 75 | } 76 | } 77 | 78 | &.old { 79 | opacity: 0; 80 | transition: opacity .5s; // REQUIRED 81 | } 82 | } 83 | } 84 | } 85 | 86 | /* Hide scrollbars in webkit... */ 87 | #story-stage::-webkit-scrollbar { 88 | display: none; 89 | } 90 | -------------------------------------------------------------------------------- /lib/gall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | import program from 'commander'; 5 | import path from 'path'; 6 | import fs from 'fs-jetpack'; 7 | // eslint-disable-next-line no-restricted-imports 8 | import 'colors'; 9 | import pug from 'pug'; 10 | import less from 'less'; 11 | import chokidar from 'chokidar'; 12 | 13 | program 14 | .version('0.3.1'); 15 | 16 | program 17 | .command('new') 18 | .option('-f, --force', 'delete existing sources/ directory if any') 19 | .description('create a new project') 20 | .action(createNew); 21 | 22 | program 23 | .command('build') 24 | .description('build the project in the current working directory') 25 | .action(build); 26 | 27 | program 28 | .command('watch') 29 | .description('watch for changes in sources/ and queue up a build when it happens') 30 | .action(watch); 31 | 32 | program 33 | .command('help') 34 | .description('show usage information') 35 | .action(() => program.outputHelp()); 36 | 37 | const SCAFFOLD_FILES = [ 38 | 'defines.json', 39 | 'style.less', 40 | 'template.pug', 41 | 'script.js' 42 | ]; 43 | 44 | function createNew(options) { 45 | const scaffoldDir = path.join(__dirname, '../scaffold'); 46 | const targetDir = path.join(process.cwd(), '/sources'); 47 | if (fs.inspect(targetDir) && !options.force) { 48 | console.log('Sources directory already exists, aborting. Use --force to override and delete its contents.'.red); 49 | return; 50 | } 51 | console.log('Copying files into scaffold dir...'.green); 52 | fs.dir(targetDir, {empty: true}); 53 | const promises = SCAFFOLD_FILES.map(filename => { 54 | const origin = path.join(scaffoldDir, filename); 55 | const target = path.join(targetDir, filename); 56 | return fs.copyAsync(origin, target) 57 | .then(() => console.log('\t', filename.yellow)); 58 | }); 59 | Promise.all(promises) 60 | .then(() => console.log('All done!'.green)); 61 | } 62 | 63 | const REQUIRED_FILES = [ 64 | 'defines.json', 65 | 'style.less', 66 | 'template.pug', 67 | 'script.js', 68 | 'story.ink.json' 69 | ]; 70 | 71 | function build() { 72 | const sourceDir = path.join(process.cwd(), '/sources'); 73 | function sourcePath(filename) { 74 | return path.join(sourceDir, filename); 75 | } 76 | const filesMissing = REQUIRED_FILES.reduce((missing, filename) => { 77 | const filepath = path.join(sourceDir, filename); 78 | if (fs.inspect(filepath)) { 79 | return missing; 80 | } 81 | missing.push(filename); 82 | return missing; 83 | }, []); 84 | if (filesMissing.length) { 85 | console.log('Error: Missing required source files'.red); 86 | filesMissing.forEach(filename => { 87 | console.log('\t', filename.red); 88 | }); 89 | } 90 | console.log('Reading source files:'.green); 91 | const data = {}; 92 | const promises = []; 93 | let template; 94 | promises.push( 95 | fs.readAsync(sourcePath('style.less')) 96 | .then(text => less.render(text)) 97 | .then(output => { 98 | console.log('\tstyle.less'.yellow); 99 | data.css = output.css; 100 | }) 101 | ); 102 | 103 | function bomStrip(text) { 104 | /* 105 | Strip a byte-order mark from the head of the file. Inklecate generates 106 | those for compatibility with some other, mostly Windows-based, tools; 107 | but since inline JSON is not, technically speaking, a file, we want 108 | to strip it out before removing it. 109 | */ 110 | if (text.charCodeAt(0) === 0xFEFF) { 111 | return text.slice(1); 112 | } 113 | return text; 114 | } 115 | 116 | promises.push( 117 | fs.readAsync(sourcePath('story.ink.json')) 118 | .then(text => { 119 | console.log('\tstory.ink.json'.yellow); 120 | data.story = bomStrip(text); 121 | }) 122 | ); 123 | promises.push( 124 | fs.readAsync(sourcePath('script.js')) 125 | .then(text => { 126 | console.log('\tscript.js'.yellow); 127 | data.script = text; 128 | }) 129 | ); 130 | promises.push( 131 | fs.readAsync(sourcePath('template.pug')) 132 | .then(text => { 133 | console.log('\ttemplate.pug'.yellow); 134 | template = text; 135 | }) 136 | ); 137 | promises.push( 138 | fs.readAsync(sourcePath('defines.json'), 'json') 139 | .then(json => { 140 | console.log('\tdefines.json'.yellow); 141 | data.defines = json; 142 | }) 143 | ); 144 | promises.push( 145 | fs.readAsync(path.join(__dirname, '../node_modules/ink-blotter/build/blotter.js')) 146 | .then(bundle => { 147 | console.log('\tblotter.js'.yellow); 148 | data.blotter = bundle; 149 | }) 150 | ); 151 | return Promise.all(promises) 152 | .then(() => { 153 | console.log('Writing output file...'.green); 154 | fs.write('out.html', pug.render(template, data)); 155 | }) 156 | .catch(err => { 157 | console.error('Error loading game data, ', err); 158 | throw err; 159 | }); 160 | } 161 | 162 | function watch() { 163 | const sourceDir = path.join(process.cwd(), '/sources'); 164 | const filesToWatch = REQUIRED_FILES 165 | .map(filename => path.join(sourceDir, filename)); 166 | const watcher = chokidar.watch(filesToWatch, {persistent: true}); 167 | let lock = false; 168 | watcher.on('change', () => { 169 | if (lock) { 170 | return; 171 | } 172 | lock = true; 173 | console.log(`Files changed on ${(new Date()).toLocaleString('en')}.`.yellow) 174 | console.log('Rebuilding...'.yellow); 175 | build().then(() => { 176 | lock = false; 177 | }); 178 | }); 179 | console.log('Watching sources/ for changes...'.blue); 180 | } 181 | 182 | program.parse(process.argv); 183 | 184 | if (!process.argv.slice(2).length) { 185 | program.outputHelp(); 186 | } 187 | --------------------------------------------------------------------------------