├── versions.js ├── solver ├── go.mod ├── go.sum ├── main.go └── solver.go ├── .gitignore ├── www ├── favicon.ico ├── sounds │ ├── beep.mp3 │ ├── laser.mp3 │ ├── warp.mp3 │ ├── buzzer.mp3 │ ├── whoosh.mp3 │ ├── approach.mp3 │ └── separate.mp3 ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── index.css ├── site.webmanifest └── index.html ├── marketing ├── cover.png ├── icon.png ├── icon.afphoto ├── cover.afdesign ├── screenshot-1.png ├── screenshot-2.png └── screenshot-3.png ├── public ├── favicon.ico ├── sounds │ ├── beep.mp3 │ ├── warp.mp3 │ ├── buzzer.mp3 │ ├── laser.mp3 │ ├── whoosh.mp3 │ ├── approach.mp3 │ └── separate.mp3 ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── index.css ├── site.webmanifest └── index.html ├── script ├── script.docx └── script.js ├── shoot-the-moon.zip ├── code ├── shoot │ ├── ease.js │ ├── back.js │ ├── meter.js │ ├── shoot.js │ ├── laser.js │ ├── text.js │ ├── stars.js │ ├── moon.js │ └── shoot.json ├── default.js ├── settings.js ├── sheet.js ├── icon.js ├── view.js ├── game.js ├── input.js ├── sounds.js ├── state.js ├── menu │ ├── stars.js │ ├── title.js │ └── menu.js ├── file.js ├── Words.js └── encrypt.js ├── sounds ├── 528863__eponn__beep-3.wav ├── 128349__kafokafo__laser.wav ├── 425728__moogy73__click01.wav ├── 453391__breviceps__warp-sfx.wav ├── 419023__jacco18__acess-denied-buzz.mp3 └── 446010__garionek__backwards-whoosh.wav ├── .vscode ├── settings.json └── launch.json ├── images ├── pixel-0.editor.json ├── ui.json ├── state.json ├── letters.json ├── ui.editor.json └── letters.editor.json ├── notes.md ├── generate ├── levels.js ├── summary.js ├── swap.js ├── zip.js ├── version.js ├── summary.md ├── colorblind.js └── index.js ├── dist ├── www.9ad09f98.css ├── www.9ad09f98.css.map ├── index.html ├── www.9ad09f98.js.map └── www.9ad09f98.js ├── README.md ├── LICENSE.md └── package.json /versions.js: -------------------------------------------------------------------------------- 1 | module.exports={css:'1',js:'1'} -------------------------------------------------------------------------------- /solver/go.mod: -------------------------------------------------------------------------------- 1 | module yopeyopey.com/solver 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | *.exe 4 | script/~$script.docx 5 | -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/favicon.ico -------------------------------------------------------------------------------- /marketing/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/marketing/cover.png -------------------------------------------------------------------------------- /marketing/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/marketing/icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /script/script.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/script/script.docx -------------------------------------------------------------------------------- /shoot-the-moon.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/shoot-the-moon.zip -------------------------------------------------------------------------------- /www/sounds/beep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/sounds/beep.mp3 -------------------------------------------------------------------------------- /www/sounds/laser.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/sounds/laser.mp3 -------------------------------------------------------------------------------- /www/sounds/warp.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/sounds/warp.mp3 -------------------------------------------------------------------------------- /marketing/icon.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/marketing/icon.afphoto -------------------------------------------------------------------------------- /public/sounds/beep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/sounds/beep.mp3 -------------------------------------------------------------------------------- /public/sounds/warp.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/sounds/warp.mp3 -------------------------------------------------------------------------------- /www/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/favicon-16x16.png -------------------------------------------------------------------------------- /www/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/favicon-32x32.png -------------------------------------------------------------------------------- /www/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/mstile-150x150.png -------------------------------------------------------------------------------- /www/sounds/buzzer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/sounds/buzzer.mp3 -------------------------------------------------------------------------------- /www/sounds/whoosh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/sounds/whoosh.mp3 -------------------------------------------------------------------------------- /marketing/cover.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/marketing/cover.afdesign -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/sounds/buzzer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/sounds/buzzer.mp3 -------------------------------------------------------------------------------- /public/sounds/laser.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/sounds/laser.mp3 -------------------------------------------------------------------------------- /public/sounds/whoosh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/sounds/whoosh.mp3 -------------------------------------------------------------------------------- /www/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/apple-touch-icon.png -------------------------------------------------------------------------------- /www/sounds/approach.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/sounds/approach.mp3 -------------------------------------------------------------------------------- /www/sounds/separate.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/sounds/separate.mp3 -------------------------------------------------------------------------------- /code/shoot/ease.js: -------------------------------------------------------------------------------- 1 | import { Ease } from 'pixi-ease' 2 | 3 | export const ease = new Ease({ noTicker: true }) -------------------------------------------------------------------------------- /marketing/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/marketing/screenshot-1.png -------------------------------------------------------------------------------- /marketing/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/marketing/screenshot-2.png -------------------------------------------------------------------------------- /marketing/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/marketing/screenshot-3.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/sounds/approach.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/sounds/approach.mp3 -------------------------------------------------------------------------------- /public/sounds/separate.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/sounds/separate.mp3 -------------------------------------------------------------------------------- /www/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/android-chrome-192x192.png -------------------------------------------------------------------------------- /www/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/www/android-chrome-512x512.png -------------------------------------------------------------------------------- /sounds/528863__eponn__beep-3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/sounds/528863__eponn__beep-3.wav -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /sounds/128349__kafokafo__laser.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/sounds/128349__kafokafo__laser.wav -------------------------------------------------------------------------------- /sounds/425728__moogy73__click01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/sounds/425728__moogy73__click01.wav -------------------------------------------------------------------------------- /sounds/453391__breviceps__warp-sfx.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/sounds/453391__breviceps__warp-sfx.wav -------------------------------------------------------------------------------- /sounds/419023__jacco18__acess-denied-buzz.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/sounds/419023__jacco18__acess-denied-buzz.mp3 -------------------------------------------------------------------------------- /sounds/446010__garionek__backwards-whoosh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/moonshot/HEAD/sounds/446010__garionek__backwards-whoosh.wav -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.ignoreWords": [ 3 | "kdeu", 4 | "qz", 5 | "tjtt", 6 | "tjtt qz kdeu" 7 | ] 8 | } -------------------------------------------------------------------------------- /images/pixel-0.editor.json: -------------------------------------------------------------------------------- 1 | {"zoom":4,"current":0,"imageData":[{"undo":[],"redo":[]}],"viewport":{"x":527.2033333333334,"y":221.60999999999996,"scale":0.5933333333333334}} 2 | -------------------------------------------------------------------------------- /code/default.js: -------------------------------------------------------------------------------- 1 | import { game } from './game' 2 | 3 | window.addEventListener('DOMContentLoaded', () => { 4 | game.start() 5 | window.addEventListener('blur', () => game.pause()) 6 | window.addEventListener('focus', () => game.resume()) 7 | }) -------------------------------------------------------------------------------- /images/ui.json: -------------------------------------------------------------------------------- 1 | {"name":"ui","imageData":[[11,11,"iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAR0lEQVQoU2NkwAL+////n5GRkRFdCkMApBCkiKBimEKCipEVYnMa2AAQQYxCsGJiFZJuMsx9xNiAEnREhwa6DQTDGVkDNsUAFNcr+F2BbxsAAAAASUVORK5CYII="]],"animations":{"idle":[[0,0]]}} 2 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | Shoot the Moon (like literally) 2 | 3 | There are too many moons. 4 | 5 | You circle around and shoot the moon. Literally. Trying to destroy it? 6 | 7 | Maybe use colors to shoot colored bullets. Definitely pixelated where each pixel is a piece (and a bullet) 8 | -------------------------------------------------------------------------------- /www/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #9f00a7 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /generate/levels.js: -------------------------------------------------------------------------------- 1 | export const levels = [ 2 | { count: 10, radius: 4, colors: 2 }, 3 | { count: 20, radius: 4, colors: 3 }, 4 | { count: 20, radius: 5, colors: 3 }, 5 | { count: 20, radius: 6, colors: 3 }, 6 | { count: 20, radius: 7, colors: 3 }, 7 | { count: 20, radius: 8, colors: 3 }, 8 | ] -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #9f00a7 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /dist/www.9ad09f98.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | -webkit-overflow-scrolling: touch; 5 | overflow: hidden; 6 | background:black; 7 | } 8 | 9 | .view { 10 | width: 100%; 11 | height: 100%; 12 | position: fixed; 13 | } 14 | 15 | /*# sourceMappingURL=/www.9ad09f98.css.map */ -------------------------------------------------------------------------------- /www/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | -webkit-overflow-scrolling: touch; 5 | overflow: hidden; 6 | background:black; 7 | } 8 | 9 | .view { 10 | width: 100%; 11 | height: 100%; 12 | position: fixed; 13 | } 14 | 15 | .version { 16 | position: absolute; 17 | bottom: 0.25rem; 18 | left: 0.25rem; 19 | color: #888888; 20 | } -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | -webkit-overflow-scrolling: touch; 5 | overflow: hidden; 6 | background:black; 7 | } 8 | 9 | .view { 10 | width: 100%; 11 | height: 100%; 12 | position: fixed; 13 | } 14 | 15 | .version { 16 | position: absolute; 17 | bottom: 0.25rem; 18 | left: 0.25rem; 19 | color: #888888; 20 | } -------------------------------------------------------------------------------- /dist/www.9ad09f98.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["index.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"www.9ad09f98.css","sourceRoot":"..\\www","sourcesContent":["body {\n padding: 0;\n margin: 0;\n -webkit-overflow-scrolling: touch;\n overflow: hidden;\n background:black;\n}\n\n.view {\n width: 100%;\n height: 100%;\n position: fixed;\n}"]} -------------------------------------------------------------------------------- /www/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /code/settings.js: -------------------------------------------------------------------------------- 1 | export const name = 'shoot_the_moon' 2 | export const encrypt = 'tjttQZKdeu' 3 | export const storageVersion = 3 4 | 5 | export const shadow = 0.5 6 | export const shadowTint = 0x888888 7 | export const uiDropTime = 1000 8 | 9 | export const release = true 10 | 11 | // debug flags 12 | export const clearStorage = release ? false : false 13 | export const state = release ? false : false 14 | export const shoot = release ? false : false 15 | export const cheat = release ? false : false -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /generate/summary.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | async function summary() { 4 | const shoot = await fs.readJSON('code/shoot/shoot.json') 5 | let s = '' 6 | for (let i = 0; i < shoot.length; i++) { 7 | const level = shoot[i] 8 | s += `${i + 1}. radius=${level.Radius} colors=${level.Colors.length} difficulty=${level.Difficulty}\n` 9 | } 10 | await fs.outputFile('generate/summary.md', s) 11 | console.log('wrote level summary to generate/summary.md.') 12 | process.exit(0) 13 | } 14 | 15 | summary() -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | Shoot the Moon 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /solver/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexflint/go-arg v1.3.0 h1:UfldqSdFWeLtoOuVRosqofU4nmhI1pYEbT4ZFS34Bdo= 2 | github.com/alexflint/go-arg v1.3.0/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= 3 | github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= 4 | github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 8 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Shoot the Moon 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /code/sheet.js: -------------------------------------------------------------------------------- 1 | import RenderSheet from 'yy-rendersheet' 2 | 3 | import letters from '../images/letters.json' 4 | import ui from '../images/ui.json' 5 | 6 | class Sheet extends RenderSheet { 7 | constructor() { 8 | super({ extrude: true, scaleMode: true }) 9 | this.letters() 10 | this.addData('arrow', ui.imageData[0][2]) 11 | } 12 | 13 | async init() { 14 | await this.asyncRender() 15 | } 16 | 17 | letters() { 18 | for (let i = 0; i < letters.imageData.length; i++) { 19 | this.addData(`letters-${i}`, letters.imageData[i][2]) 20 | } 21 | } 22 | } 23 | 24 | export const sheet = new Sheet() -------------------------------------------------------------------------------- /generate/swap.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const filename = 'code/shoot/shoot.json' 4 | 5 | async function swap() { 6 | const l1 = parseInt(process.argv[2]) - 1 7 | const l2 = parseInt(process.argv[3]) - 1 8 | if (isNaN(l1) || isNaN(l2)) { 9 | console.log('node swap ') 10 | process.exit(0) 11 | } 12 | const shoot = await fs.readJSON(filename) 13 | const swap = shoot[l1] 14 | shoot[l1] = shoot[l2] 15 | shoot[l2] = swap 16 | await fs.outputJSON(filename, shoot) 17 | console.log(`swapped levels ${l1 + 1} with ${l2 + 1} and wrote shoot.json.`) 18 | process.exit(0) 19 | } 20 | 21 | swap() -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | Shoot the Moon -------------------------------------------------------------------------------- /generate/zip.js: -------------------------------------------------------------------------------- 1 | const archiver = require('archiver') 2 | const fs = require('fs-extra') 3 | 4 | /** 5 | * from https://stackoverflow.com/a/51518100/1955997 6 | * @param {String} source 7 | * @param {String} out 8 | * @returns {Promise} 9 | */ 10 | function zipDirectory(source, out) { 11 | const archive = archiver('zip', { zlib: { level: 9 }}); 12 | const stream = fs.createWriteStream(out); 13 | 14 | return new Promise((resolve, reject) => { 15 | archive 16 | .directory(source, false) 17 | .on('error', err => reject(err)) 18 | .pipe(stream) 19 | ; 20 | 21 | stream.on('close', () => resolve()); 22 | archive.finalize(); 23 | }); 24 | } 25 | 26 | async function start() { 27 | await zipDirectory('www', 'shoot-the-moon.zip') 28 | console.log('generated shoot-the-moon.zip') 29 | process.exit(0) 30 | } 31 | 32 | start() -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run Solver", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/solver", 13 | "env": {}, 14 | "args": [ 15 | "-- radius 3", 16 | "--colors 2" 17 | ] 18 | }, 19 | { 20 | "name": "Colorblind", 21 | "type": "node", 22 | "request": "launch", 23 | "program": "${workspaceFolder}/generate/colorblind.js", 24 | "env": {}, 25 | "args": [] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shoot the Moon (like literally) 2 | 3 | ## Game Jam 4 | An open-source game coded for [Github's Game Off 2020](https://itch.io/jam/game-off-2020). 5 | 6 | ## How to Play 7 | Destroy a moon by shooting a laser at the moon in the fewest possible shots. The laser destroys all neighboring blocks of the same color, and the moon's gravity collapses the remaining blocks toward the center. You have a limited number of shots for each moon. 8 | 9 | ## Play Development Build 10 | https://yopeyopey.com/prototypes/moonshot/ 11 | 12 | ## Install Instructions 13 | 1. `git clone git@github.com:davidfig/moonshot.git` 14 | 2. `cd moonshot` 15 | 3. `npm install` 16 | 4. `npm run serve` 17 | 5. open browser to http://localhost:8888/ 18 | 19 | ## Licenses 20 | Source code: MIT License 21 | Game script and game assets: Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) 22 | 23 | (c) 2020 [YOPEY YOPEY LLC](https://yopeyopey.com/) by [David Figatner](https://twitter.com/yopey_yopey/) 24 | -------------------------------------------------------------------------------- /generate/version.js: -------------------------------------------------------------------------------- 1 | const bump = require('json-bump') 2 | const readline = require('readline') 3 | 4 | const packageJson = require('../package.json') 5 | 6 | async function start() { 7 | const rl = readline.createInterface({ 8 | input: process.stdin, 9 | output: process.stdout, 10 | }) 11 | const version = packageJson.version 12 | const parts = version.split('.') 13 | const updated = `${parts[0]}.${parts[1]}.${parseInt(parts[2]) + 1}` 14 | rl.question( 15 | `Current version: ${version}\nUpdated version: `, 16 | async selected => { 17 | if (selected !== version) { 18 | await bump('package.json', { 19 | replace: selected 20 | }) 21 | console.log( 22 | `Writing version ${selected} to package.json` 23 | ) 24 | } 25 | rl.close() 26 | process.exit(0) 27 | } 28 | ) 29 | rl.write(updated) 30 | } 31 | 32 | start() -------------------------------------------------------------------------------- /code/shoot/back.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | 3 | import { sheet } from '../sheet' 4 | import { file } from '../file' 5 | import { Words } from '../Words' 6 | import { state } from '../state' 7 | import { sounds } from '../sounds' 8 | 9 | class Back extends PIXI.Container { 10 | constructor() { 11 | super() 12 | this.arrow = this.addChild(sheet.get('arrow')) 13 | this.arrow.anchor.set(0) 14 | this.arrow.tint = 0x888888 15 | this.arrow.width = this.arrow.height = 1 / 11 * 2 16 | this.level = this.addChild(new Words()) 17 | this.x = 1 18 | } 19 | 20 | get size() { 21 | return this.x + this.width 22 | } 23 | 24 | getScale() { 25 | return this.level.scale.x 26 | } 27 | 28 | change() { 29 | this.level.change(`level ${file.shootLevel + 1}`) 30 | this.level.height = this.arrow.height 31 | this.level.scale.x = this.level.scale.y 32 | this.level.x = this.arrow.width + this.arrow.x + 1 33 | } 34 | 35 | down(local) { 36 | if (local.x <= this.width + 1 && local.y < this.height + 1) { 37 | state.change('menu') 38 | sounds.play('beep') 39 | return true 40 | } 41 | } 42 | } 43 | 44 | export const back = new Back() -------------------------------------------------------------------------------- /code/icon.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | import random from 'yy-random' 3 | 4 | import { view } from './view' 5 | import { Words } from './Words' 6 | 7 | class Icon extends PIXI.Graphics { 8 | init() { 9 | // this.halfMoon() 10 | this.by() 11 | view.stage.addChild(this) 12 | view.update() 13 | } 14 | 15 | by() { 16 | const words = this.addChild(new Words('a game by David Figatner', { shadow: true })) 17 | words.scale.set(0.5) 18 | words.position.set(1, 1) 19 | view.update() 20 | } 21 | 22 | halfMoon() { 23 | const radius = 8 24 | const colors = [0xC44BE5, 0xD3306C, 0x18365F] 25 | const radiusSquared = radius * radius 26 | for (let y = 0; y <= radius * 2; y++) { 27 | for (let x = 0; x <= radius * 2; x++) { 28 | const dx = x - radius 29 | const dy = y - radius 30 | const distanceSquared = dx*dx + dy*dy 31 | if (distanceSquared <= radiusSquared) { 32 | this 33 | .beginFill(random.pick(colors), x < radius ? 1 : 0.5) 34 | .drawRect(x, y, 1, 1) 35 | .endFill() 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | export const icon = new Icon() -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All assets except source code, including game script and graphical assets, are licensed under the Attribution-NonCommercial-NoDerivatives 4.0 International: 2 | 3 | Copyright (c) 2020 YOPEY YOPEY LLC by David Figatner 4 | 5 | The full text of the license is available at https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode 6 | 7 | Non-legal summary: 8 | 9 | 1. Attribution is required 10 | 2. Noncommercial use only 11 | 3. No distributed derivatives 12 | 13 | === 14 | 15 | All source code is licensed under MIT: 16 | 17 | The MIT License (MIT) 18 | 19 | Copyright (c) 2020 YOPEY YOPEY LLC by David Figatner 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, damaging -------------------------------------------------------------------------------- /code/view.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | 3 | import * as settings from './settings' 4 | import packageJson from '../package.json' 5 | 6 | const size = 50 7 | 8 | class View { 9 | init() { 10 | this.renderer = new PIXI.Renderer({ 11 | view: document.querySelector('.view'), 12 | resolution: window.devicePixelRatio, 13 | transparent: true, 14 | antialias: true, 15 | }) 16 | this.stage = new PIXI.Container() 17 | this.resize() 18 | window.addEventListener('contextmenu', e => e.preventDefault()) 19 | if (!settings.release) { 20 | const div = document.createElement('div') 21 | div.innerHTML = `v${packageJson.version}` 22 | div.className = 'version' 23 | document.body.appendChild(div) 24 | } 25 | } 26 | 27 | get width() { 28 | return Math.floor(window.innerWidth / this.stage.scale.x) 29 | } 30 | 31 | get height() { 32 | return Math.floor(window.innerHeight / this.stage.scale.x) 33 | } 34 | 35 | get size() { 36 | return size 37 | } 38 | 39 | resize() { 40 | this.stage.scale.set((window.innerWidth > window.innerHeight ? window.innerWidth : window.innerHeight) / size) 41 | this.renderer.resize(window.innerWidth, window.innerHeight) 42 | this.max = Math.max(window.innerWidth, window.innerHeight) / this.stage.scale.x 43 | } 44 | 45 | update() { 46 | this.renderer.render(this.stage) 47 | } 48 | } 49 | 50 | export const view = new View() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moonshot", 3 | "version": "1.0.1", 4 | "description": "Game Off 2020 - Shoot the Moon (like literally)", 5 | "scripts": { 6 | "serve": "node generate", 7 | "solve": "cd solver && go build && solver", 8 | "build": "node generate --production", 9 | "deploy": "node generate/version && npm run build && wsl.exe rsync -rvW --delete -e ssh public/* sewcrates@67.207.92.70:/home/sewcrates/prototypes/moonshot", 10 | "play": "node generate/version && npm run build && wsl.exe rsync -rvW --delete -e ssh public/* sewcrates@67.207.92.70:/home/sewcrates/play/shoot-the-moon", 11 | "zip": "node generate/zip", 12 | "colorblind": "node generate/colorblind", 13 | "summary": "node generate/colorblind && node generate/summary" 14 | }, 15 | "author": "David Figatner", 16 | "license": "MIT", 17 | "dependencies": { 18 | "archiver": "^5.1.0", 19 | "cuid": "^2.1.8", 20 | "fs-extra": "^9.0.1", 21 | "localforage": "^1.9.0", 22 | "pixi-ease": "^3.0.6", 23 | "pixi-sound": "^3.0.5", 24 | "pixi.js": "^5.3.3", 25 | "yy-fps": "^1.1.0", 26 | "yy-pixel": "^2.5.0", 27 | "yy-random": "^1.9.1", 28 | "yy-rendersheet": "^5.0.5" 29 | }, 30 | "devDependencies": { 31 | "chokidar": "^3.4.3", 32 | "color": "^3.1.3", 33 | "color-difference": "^0.3.4", 34 | "esbuild": "^0.8.5", 35 | "express": "^4.17.1", 36 | "intersects": "^2.7.2", 37 | "json-bump": "^1.0.2", 38 | "safe-dye": "^1.0.3" 39 | } 40 | } -------------------------------------------------------------------------------- /code/game.js: -------------------------------------------------------------------------------- 1 | import FPS from 'yy-fps' 2 | 3 | import { file } from './file' 4 | import { state } from './state' 5 | import { view } from './view' 6 | import { sheet } from './sheet' 7 | import { input } from './input' 8 | import { sounds } from './sounds' 9 | import { icon } from './icon' 10 | import * as settings from './settings' 11 | 12 | class Game { 13 | async start() { 14 | sounds.load() 15 | await file.init() 16 | await sheet.init() 17 | view.init() 18 | icon.init() 19 | state.init() 20 | if (!settings.release) { 21 | this.fps = new FPS() 22 | } 23 | this.update() 24 | input.init() 25 | window.addEventListener('resize', () => this.resize()) 26 | } 27 | 28 | pause() { 29 | if (this.raf) { 30 | this.paused = true 31 | cancelAnimationFrame(this.raf) 32 | sounds.pause() 33 | this.raf = null 34 | } 35 | } 36 | 37 | resume() { 38 | this.paused = false 39 | sounds.resume() 40 | if (!this.raf) { 41 | this.update() 42 | } 43 | } 44 | 45 | resize() { 46 | view.resize() 47 | state.resize() 48 | if (this.paused) { 49 | view.update() 50 | } 51 | } 52 | 53 | update() { 54 | if (!this.paused) { 55 | state.update() 56 | view.update() 57 | if (this.fps) { 58 | this.fps.frame() 59 | } 60 | this.raf = requestAnimationFrame(() => this.update()) 61 | } 62 | } 63 | } 64 | 65 | export const game = new Game() -------------------------------------------------------------------------------- /code/input.js: -------------------------------------------------------------------------------- 1 | import { state } from './state' 2 | 3 | class Input { 4 | init() { 5 | const view = document.querySelector('.view') 6 | view.addEventListener('mousedown', e => this.down(e)) 7 | view.addEventListener('mousemove', e => this.move(e)) 8 | view.addEventListener('mouseup', e => this.up(e)) 9 | 10 | view.addEventListener('touchstart', e => this.down(e)) 11 | view.addEventListener('touchmove', e => this.move(e)) 12 | view.addEventListener('touchend', e => this.up(e)) 13 | 14 | window.addEventListener('keydown', e => this.keyDown(e)) 15 | window.addEventListener('keydown', e => this.keyUp(e)) 16 | } 17 | 18 | translateEvent(e) { 19 | let x, y 20 | if (typeof e.touches === 'undefined') { 21 | x = e.clientX 22 | y = e.clientY 23 | } else { 24 | if (e.touches.length) { 25 | x = e.touches[0].clientX 26 | y = e.touches[0].clientY 27 | } else { 28 | x = e.changedTouches[0].clientX 29 | y = e.changedTouches[0].clientY 30 | } 31 | } 32 | return { x, y } 33 | } 34 | 35 | down(e) { 36 | const point = this.translateEvent(e) 37 | state.down(point) 38 | e.preventDefault() 39 | } 40 | 41 | move(e) { 42 | const point = this.translateEvent(e) 43 | state.move(point) 44 | } 45 | 46 | up(e) { 47 | const point = this.translateEvent(e) 48 | state.up(point) 49 | } 50 | 51 | keyDown(e) { 52 | switch (e.code) {} 53 | } 54 | 55 | keyUp(e) { 56 | switch (e.code) {} 57 | } 58 | } 59 | 60 | export const input = new Input() -------------------------------------------------------------------------------- /code/sounds.js: -------------------------------------------------------------------------------- 1 | import pixiSound from 'pixi-sound' 2 | 3 | import { file } from './file' 4 | 5 | const SOUNDS = ['laser', 'buzzer', 'warp', 'whoosh', 'beep', 'separate', 'approach'] 6 | const SPRITES = {} 7 | 8 | class Sounds { 9 | load() { 10 | this.list = [] 11 | this.count = 0 12 | for (const name of SOUNDS) { 13 | this.sound(name) 14 | } 15 | for (const name in SPRITES) { 16 | this.sprite(name, SPRITES[name]) 17 | } 18 | } 19 | 20 | loaded() { 21 | this.count-- 22 | if (this.count === 0) { 23 | this.ready = true 24 | } 25 | } 26 | 27 | sound(name) { 28 | this.list[name] = pixiSound.Sound.from({ 29 | url: 'sounds/' + name + '.mp3', 30 | preload: true, 31 | loaded: () => this.loaded(), 32 | }) 33 | this.count++ 34 | } 35 | 36 | sprite(name, sprites) { 37 | this.list[name] = pixiSound.Sound.from({ 38 | url: 'sounds/' + name + '.mp3', 39 | preload: true, 40 | loaded: () => this.loaded(), 41 | sprites 42 | }) 43 | this.count++ 44 | } 45 | 46 | play(name, options) { 47 | options = options || {} 48 | if (this.ready && file.sound) { 49 | if (options.sprite) { 50 | return this.list[name].play(options.sprite, options) 51 | } else { 52 | return this.list[name].play(options) 53 | } 54 | } 55 | } 56 | 57 | stop(name) { 58 | this.list[name].stop() 59 | } 60 | 61 | fade(name, duration, id) { 62 | if (file.sound) { 63 | this.list[name].fade(1, 0, duration, id) 64 | } 65 | } 66 | 67 | pause() { 68 | pixiSound.pauseAll() 69 | } 70 | 71 | resume() { 72 | pixiSound.resumeAll() 73 | } 74 | } 75 | 76 | export const sounds = new Sounds() -------------------------------------------------------------------------------- /code/state.js: -------------------------------------------------------------------------------- 1 | import { file } from './file' 2 | import { shoot } from './shoot/shoot' 3 | import { menu } from './menu/menu' 4 | import { view } from './view' 5 | import { story } from '../script/script' 6 | import * as settings from './settings' 7 | 8 | class State { 9 | init() { 10 | this.states = { 11 | 'menu': menu, 12 | 'shoot': shoot, 13 | } 14 | for (const key in this.states) { 15 | this.states[key].init() 16 | } 17 | this.state = settings.state || 'menu' 18 | } 19 | 20 | set state(state) { 21 | if (state !== this._state) { 22 | if (this._state) { 23 | view.stage.removeChild(this.states[this.state]) 24 | this.states[this.state].reset() 25 | } 26 | this._state = state 27 | view.stage.addChild(this.states[this.state]) 28 | this.states[this.state].change() 29 | } 30 | } 31 | get state() { 32 | return this._state 33 | } 34 | 35 | next() { 36 | if (this.state === 'shoot') { 37 | if (file.shootLevel === story.length - 1) { 38 | this.states[this.state].endScreen() 39 | } else { 40 | file.shootLevel++ 41 | this.states[this.state].change(true) 42 | } 43 | } 44 | } 45 | 46 | resize() { 47 | for (const key in this.states) { 48 | this.states[key].resize() 49 | } 50 | } 51 | 52 | change(state) { 53 | this.state = state 54 | } 55 | 56 | down(point) { 57 | this.states[this.state].down(point) 58 | } 59 | 60 | move(point) { 61 | this.states[this.state].move(point) 62 | 63 | } 64 | 65 | up(point) { 66 | this.states[this.state].up(point) 67 | } 68 | 69 | update() { 70 | this.states[this.state].update() 71 | } 72 | 73 | } 74 | 75 | export const state = new State() -------------------------------------------------------------------------------- /generate/summary.md: -------------------------------------------------------------------------------- 1 | 1. radius=4 colors=2 difficulty=0 2 | 2. radius=4 colors=2 difficulty=1 3 | 3. radius=4 colors=2 difficulty=1 4 | 4. radius=4 colors=2 difficulty=1 5 | 5. radius=4 colors=2 difficulty=1 6 | 6. radius=4 colors=2 difficulty=1 7 | 7. radius=4 colors=2 difficulty=1 8 | 8. radius=4 colors=2 difficulty=2 9 | 9. radius=5 colors=2 difficulty=2 10 | 10. radius=5 colors=2 difficulty=2 11 | 11. radius=4 colors=3 difficulty=1 12 | 12. radius=4 colors=3 difficulty=1 13 | 13. radius=4 colors=3 difficulty=1 14 | 14. radius=4 colors=3 difficulty=1 15 | 15. radius=4 colors=3 difficulty=1 16 | 16. radius=4 colors=3 difficulty=1 17 | 17. radius=4 colors=3 difficulty=1 18 | 18. radius=4 colors=3 difficulty=2 19 | 19. radius=4 colors=3 difficulty=2 20 | 20. radius=4 colors=3 difficulty=2 21 | 21. radius=4 colors=3 difficulty=3 22 | 22. radius=4 colors=3 difficulty=4 23 | 23. radius=5 colors=3 difficulty=2 24 | 24. radius=5 colors=3 difficulty=2 25 | 25. radius=5 colors=3 difficulty=2 26 | 26. radius=5 colors=3 difficulty=2 27 | 27. radius=5 colors=3 difficulty=2 28 | 28. radius=5 colors=3 difficulty=2 29 | 29. radius=5 colors=3 difficulty=2 30 | 30. radius=5 colors=3 difficulty=3 31 | 31. radius=5 colors=3 difficulty=3 32 | 32. radius=5 colors=3 difficulty=3 33 | 33. radius=5 colors=3 difficulty=3 34 | 34. radius=5 colors=3 difficulty=3 35 | 35. radius=5 colors=3 difficulty=3 36 | 36. radius=5 colors=3 difficulty=4 37 | 37. radius=5 colors=3 difficulty=4 38 | 38. radius=5 colors=3 difficulty=5 39 | 39. radius=5 colors=3 difficulty=5 40 | 40. radius=5 colors=3 difficulty=5 41 | 41. radius=5 colors=3 difficulty=6 42 | 42. radius=5 colors=3 difficulty=6 43 | 43. radius=5 colors=3 difficulty=7 44 | 44. radius=5 colors=3 difficulty=7 45 | 45. radius=5 colors=3 difficulty=9 46 | 46. radius=5 colors=3 difficulty=9 47 | 47. radius=5 colors=3 difficulty=10 48 | 48. radius=6 colors=3 difficulty=3 49 | 49. radius=6 colors=3 difficulty=4 50 | 50. radius=6 colors=3 difficulty=4 51 | 51. radius=6 colors=3 difficulty=4 52 | 52. radius=6 colors=3 difficulty=5 53 | -------------------------------------------------------------------------------- /code/menu/stars.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | import random from 'yy-random' 3 | import intersects from 'intersects' 4 | 5 | import { view } from '../view' 6 | 7 | const count = 30 8 | const maxTwinkle = 0.1 9 | 10 | class Stars extends PIXI.Container { 11 | constructor() { 12 | super() 13 | } 14 | 15 | overlap(star) { 16 | for (const check of this.children) { 17 | if (check !== star && intersects.boxBox(check.x - 0.5, check.y - 0.5, 1, 1, star.x - 0.5, star.y - 0.5, 1, 1)) { 18 | return true 19 | } 20 | } 21 | } 22 | 23 | draw() { 24 | random.reset() 25 | for (let i = 0; i < count; i++) { 26 | const star = this.addChild(new PIXI.Sprite(PIXI.Texture.WHITE)) 27 | star.anchor.set(0.5) 28 | do { 29 | star.location = [random.get(1, true), random.get(1, true)] 30 | star.position.set(0.5 + star.location[0] * (view.width - 0.5), 0.5 + star.location[1] * (view.height - 0.5)) 31 | } while (this.overlap(star)) 32 | star.width = star.height = 1 33 | star.alpha = star.alphaSave = random.range(0.2, 0.75, true) 34 | star.twinkle = random.range(0.01, 0.02) 35 | star.direction = random.sign() 36 | } 37 | } 38 | 39 | resize() { 40 | for (const star of this.children) { 41 | star.position.set(0.5 + star.location[0] * (view.width - 1), 0.5 + star.location[1] * (view.height - 1)) 42 | } 43 | } 44 | 45 | 46 | update() { 47 | for (const star of this.children) { 48 | if (star.direction === 1) { 49 | star.alpha += star.twinkle 50 | if (star.alpha >= star.alphaSave + maxTwinkle) { 51 | star.direction = -1 52 | star.alpha = star.alphaSave + maxTwinkle 53 | } 54 | } else { 55 | star.alpha -= star.twinkle 56 | if (star.alpha <= star.alphaSave - maxTwinkle) { 57 | star.direction = 1 58 | star.alpha = star.alphaSave - maxTwinkle 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | export const stars = new Stars() 66 | -------------------------------------------------------------------------------- /code/menu/title.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | import random from 'yy-random' 3 | 4 | import { view } from '../view' 5 | import { Words } from '../Words' 6 | 7 | const padding = 0.75 8 | const colorCount = 3 9 | const radius = 6 10 | 11 | class Title extends PIXI.Container { 12 | init() { 13 | this.halfMoon() 14 | this.title = this.addChild(new Words('shoot the moon', { shadow: true })) 15 | 16 | // todo: fix this for other orientations 17 | this.title.width = view.width / 2 18 | this.title.scale.y = this.title.scale.x 19 | this.subtitle = this.addChild(new Words('(like literally)', { shadow: true, color: 0xdddddd })) 20 | this.subtitle.width = this.title.width 21 | this.subtitle.scale.y = this.subtitle.scale.x 22 | this.subtitle.y = this.title.height + padding 23 | this.moon.height = this.title.height + this.subtitle.height + padding 24 | this.moon.scale.x = this.moon.scale.y 25 | this.title.x = this.subtitle.x = this.moon.width + 0.5 26 | this.position.set(view.width / 2 - this.width / 2, 1) 27 | } 28 | 29 | halfMoon() { 30 | const colors = [] 31 | for (let i = 0; i < colorCount; i++) { 32 | colors.push(random.get(0xffffff)) 33 | } 34 | this.moon = this.addChild(new PIXI.Graphics()) 35 | const radiusSquared = radius * radius 36 | for (let y = 0; y <= radius * 2; y++) { 37 | for (let x = 0; x <= radius * 2; x++) { 38 | const dx = x - radius 39 | const dy = y - radius 40 | const distanceSquared = dx*dx + dy*dy 41 | if (distanceSquared <= radiusSquared) { 42 | if (x < radius) { 43 | this.moon 44 | .beginFill(random.pick(colors)) 45 | .drawRect(x, y, 1, 1) 46 | .endFill() 47 | } else { 48 | this.moon 49 | .beginFill(0) 50 | .drawRect(x, y, 1, 1) 51 | .endFill() 52 | .beginFill(random.pick(colors), 0.5) 53 | .drawRect(x, y, 1, 1) 54 | .endFill() 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | export const title = new Title() -------------------------------------------------------------------------------- /generate/colorblind.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const safeDye = require('safe-dye') 3 | const random = require('yy-random') 4 | const Color = require('color') 5 | 6 | // add back if needed 7 | // const colorDiff = require('color-difference') 8 | 9 | const file = 'code/shoot/shoot.json' 10 | 11 | function toHex(n) { 12 | let s = n.toString(16) 13 | while (s.length < 6) { 14 | s = `0${s}` 15 | } 16 | return `#${s}` 17 | } 18 | 19 | async function start() { 20 | const shoot = await fs.readJSON(file) 21 | let count = 0 22 | for (let i = 0; i < shoot.length; i++) { 23 | const entry = shoot[i] 24 | let fail, first = true 25 | do { 26 | fail = false 27 | for (const c of entry.Colors) { 28 | const translate = Color(toHex(c)) 29 | if (translate.luminosity() <= 0.179) { 30 | fail = true 31 | break 32 | } 33 | } 34 | if (!fail) { 35 | for (let j = 0; j < entry.Colors.length - 1; j++) { 36 | for (let k = j + 1; k < entry.Colors.length; k++) { 37 | const c1 = toHex(entry.Colors[j]) 38 | const c2 = toHex(entry.Colors[k]) 39 | if (!safeDye.validate(c1, c2)) { // || colorDiff.compare(c1, c2) < 10) { 40 | fail = true 41 | break 42 | } 43 | } 44 | if (fail) { 45 | break 46 | } 47 | } 48 | } 49 | if (fail) { 50 | for (let k = 0; k < entry.Colors.length; k++) { 51 | entry.Colors[k] = random.get(0xffffff) 52 | } 53 | if (first) count++ 54 | } 55 | first = false 56 | } while (fail) 57 | } 58 | shoot.sort((a, b) => { 59 | if (a.Colors.length < b.Colors.length) { 60 | return -1 61 | } 62 | if (a.Colors.length > b.Colors.length) { 63 | return 1 64 | } 65 | if (a.Radius < b.Radius) { 66 | return -1 67 | } 68 | if (a.Radius > b.Radius) { 69 | return 1 70 | } 71 | if (a.Difficulty < b.Difficulty) { 72 | return -1 73 | } 74 | if (a.Difficulty > b.Difficulty) { 75 | return 1 76 | } 77 | return 0 78 | }) 79 | await fs.outputJSON(file, shoot) 80 | console.log(`Fixed ${count} color issues with shoot.json.`) 81 | process.exit(0) 82 | } 83 | 84 | start() -------------------------------------------------------------------------------- /images/state.json: -------------------------------------------------------------------------------- 1 | {"tool":"paint","cursorX":2,"cursorY":0,"cursorSizeX":1,"cursorSizeY":1,"foreground":"ffffffff","isForeground":true,"background":"00000000","lastFiles":["letters.json","ui.json","pixel-0.json"],"manager":{"zoom":4,"images":true,"alphabetical":true},"views":[{"frames":{"x":0,"y":685,"width":200,"height":200,"order":0},"toolbar":{"x":0,"y":5,"order":1},"palette":{"x":1295,"y":310,"width":200,"height":150,"order":2},"picker":{"x":1295,"y":5,"width":200,"height":300,"order":3},"info":{"x":1295,"y":680,"order":4},"animation":{"x":1055,"y":5,"width":230,"height":226,"closed":true,"order":5},"position":{"x":1295,"y":615,"order":6},"manager":{"x":55,"y":5,"width":194,"height":250,"closed":false,"order":7},"keyboard":{"x":450,"y":245,"width":600,"height":400,"closed":true,"order":8},"outline":{"x":0,"y":0,"closed":true,"order":9}},{"frames":{"x":0,"y":685,"width":200,"height":200,"closed":true,"order":0},"toolbar":{"x":0,"y":5,"closed":true,"order":1},"palette":{"x":1295,"y":310,"width":200,"height":150,"closed":true,"order":2},"picker":{"x":1295,"y":5,"width":200,"height":300,"closed":true,"order":3},"info":{"x":1295,"y":680,"closed":true,"order":4},"animation":{"x":1055,"y":5,"width":230,"height":226,"closed":true,"order":5},"position":{"x":1295,"y":615,"order":6,"closed":true},"manager":{"x":55,"y":5,"width":194,"height":250,"closed":true,"order":7},"keyboard":{"x":450,"y":245,"width":600,"height":400,"closed":true,"order":8},"outline":{"x":0,"y":0,"closed":true,"order":9}}],"view":0,"relative":"top-left","keys":{"New":"CommandOrControl+M|CommandOrControl+N","Open":"CommandOrControl+O","Save":"CommandOrControl+S","Export":"CommandOrControl+E","Exit":"CommandOrControl+Q","Undo":"CommandOrControl+Z","Redo":"CommandOrControl+Shift+Z","Copy":"CommandOrControl+C","Cut":"CommandOrControl+X","Paste":"CommandOrControl+V","SelectAll":"CommandOrControl+A","Draw":"space","Dropper":"i","Clear":"escape","Erase":"backspace","SwapForeground":"x","SelectTool":"v","PaintTool":"b","FillTool":"f","CircleTool":"c","EllipseTool":"e","LineTool":"l","CropTool":"z","NextView":"Tab","PreviousView":"shift+tab","ToolbarWindow":"","InfoWindow":"","AnimationWindow":"a","PaletteWindow":"","PickerWindow":"","FramesWindow":"","PositionWindow":"","ManagerWindow":"m","KeyboardWindow":"ctrl+k","ResetWindows":"ctrl+p","AddFrame":"CommandOrControl+F","DeleteFrame":"CommandOrControl+Backspace","NewFrame":"CommandOrControl+F","Duplicate":"CommandOrControl+D","Delete":"CommandOrControl+Backspace","NextFrame":"CommandOrControl+arrowright","PreviousFrame":"CommandOrControl+arrowleft","Clockwise":"CommandOrControl+period","CounterClockwise":"CommandOrControl+comma","FlipHorizontal":"CommandOrControl+H","FlipVertical":"CommandOrControl+B"},"lastFile":"letters.json"} 2 | -------------------------------------------------------------------------------- /code/file.js: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage' 2 | import Encrypt from './encrypt' 3 | import cuid from 'cuid' 4 | 5 | import * as settings from './settings' 6 | 7 | class File { 8 | 9 | init() { 10 | return new Promise(resolve => { 11 | localforage.config({ name: settings.name, storeName: settings.name }) 12 | if (settings.clearStorage) { 13 | localforage.clear() 14 | this.erase() 15 | resolve() 16 | } else { 17 | localforage.getItem('data', (err, saved) => { 18 | if (saved) { 19 | try { 20 | this.data = JSON.parse(Encrypt.decrypt(saved, settings.encrypt)) 21 | if (this.data.version !== settings.storageVersion) { 22 | this.upgradeStorage() 23 | } 24 | resolve() 25 | } catch (e) { 26 | console.warn('erasing storage because of error in file...', e) 27 | this.erase() 28 | resolve() 29 | } 30 | } else { 31 | this.erase() 32 | resolve() 33 | } 34 | }) 35 | } 36 | }) 37 | } 38 | 39 | async erase() { 40 | this.data = { 41 | version: settings.storageVersion, 42 | sound: 1, 43 | user: cuid(), 44 | shoot: { 45 | level: 0, 46 | max: 0, 47 | }, 48 | noStory: false, 49 | } 50 | await this.save() 51 | } 52 | 53 | get shoot() { 54 | return this.data.shoot 55 | } 56 | 57 | get shootLevel() { 58 | return this.data.shoot.level 59 | } 60 | set shootLevel(value) { 61 | if (value !== this.data.shoot.level) { 62 | this.data.shoot.level = value 63 | this.data.shoot.max = Math.max(this.data.shoot.level, this.data.shoot.max) 64 | this.save() 65 | } 66 | } 67 | 68 | get shootMax() { 69 | return this.data.shoot.max 70 | } 71 | 72 | get sound() { 73 | return this.data.sound 74 | } 75 | set sound(value) { 76 | this.data.sound = value 77 | this.save() 78 | } 79 | 80 | get noStory() { 81 | return this.data.noStory 82 | } 83 | set noStory(value) { 84 | this.data.noStory = value 85 | this.save() 86 | } 87 | 88 | async save() { 89 | return new Promise(resolve => { 90 | localforage.setItem('data', Encrypt.encrypt(JSON.stringify(this.data), settings.encrypt), resolve) 91 | }) 92 | } 93 | 94 | upgradeStorage() { 95 | this.erase() 96 | } 97 | } 98 | 99 | export const file = new File() -------------------------------------------------------------------------------- /generate/index.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild') 2 | const chokidar = require('chokidar') 3 | const express = require('express') 4 | const fs = require('fs-extra') 5 | 6 | const packageJson = require('../package.json') 7 | 8 | const port = 8888 9 | 10 | async function compile() { 11 | await esbuild.build({ 12 | entryPoints: ['code/default.js'], 13 | bundle: true, 14 | sourcemap: true, 15 | outfile: 'www/index.js', 16 | }) 17 | const now = new Date() 18 | console.log(`[${now.toLocaleString()}] compiled index.js`) 19 | } 20 | 21 | async function html() { 22 | const s = '' + 23 | '' + 24 | '' + 25 | '' + 26 | '' + 27 | '' + 28 | '' + 29 | '' + 30 | '' + 31 | '' + 32 | '' + 33 | 'Shoot the Moon' + 34 | `` + 35 | '' 36 | await fs.outputFile('public/index.html', s) 37 | } 38 | 39 | async function build() { 40 | await esbuild.build({ 41 | entryPoints: ['code/default.js'], 42 | bundle: true, 43 | sourcemap: false, 44 | outfile: `public/index.${packageJson.version}.js`, 45 | minify: true, 46 | }) 47 | const now = new Date() 48 | console.log(`[${now.toLocaleString()}] compiled index.${packageJson.version}.js`) 49 | html() 50 | } 51 | 52 | function watch() { 53 | compile() 54 | const watcher = chokidar.watch(['code/**/*', 'script/script.js']) 55 | watcher.on('change', compile) 56 | } 57 | 58 | function serve() { 59 | const app = express() 60 | app.use(express.static('www')) 61 | app.listen(port) 62 | console.log(`Shoot the Moon (like literally) - debug server running at http://localhost:${port}...`) 63 | } 64 | 65 | const files = [ 66 | 'android-chrome-192x192.png', 67 | 'android-chrome-512x512.png', 68 | 'apple-touch-icon.png', 69 | 'browserconfig.xml', 70 | 'favicon.ico', 71 | 'favicon-16x16.png', 72 | 'favicon-32x32.png', 73 | 'index.css', 74 | 'mstile-150x150.png', 75 | 'site.webmanifest', 76 | ] 77 | 78 | async function start() { 79 | if (process.argv[2] === '--production') { 80 | console.log('Building Shoot the Moon (like literally) for production...') 81 | await fs.emptyDir('public') 82 | for (const file of files) { 83 | await fs.copy(`www/${file}`, `public/${file}`) 84 | } 85 | await fs.copy('www/sounds', 'public/sounds/') 86 | build() 87 | } else { 88 | watch() 89 | serve() 90 | } 91 | } 92 | 93 | start() -------------------------------------------------------------------------------- /dist/www.9ad09f98.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../node_modules/parcel/src/builtins/bundle-url.js","../node_modules/parcel/src/builtins/css-loader.js"],"names":["bundleURL","getBundleURLCached","getBundleURL","Error","err","matches","stack","match","getBaseURL","url","replace","exports","bundle","require","updateLink","link","newLink","cloneNode","onload","remove","href","split","Date","now","parentNode","insertBefore","nextSibling","cssTimeout","reloadCSS","setTimeout","links","document","querySelectorAll","i","length","module"],"mappings":"AAAA,ACAA,IDAIA,ACAAY,MAAM,GDAG,ACAAC,GDAG,IAAhB,ACAoB,CAAC,cAAD,CAApB;;ADCA,ACCA,SDDSZ,ACCAa,UAAT,CAAoBC,IAApB,EAA0B,CDD1B,GAA8B;AAC5B,ACCA,MDDI,ACCAC,CDDChB,MCCM,GDDX,ACCce,EDDE,ECCE,CAACE,SAAL,EAAd;ADAEjB,IAAAA,SAAS,GAAGE,YAAY,EAAxB;AACD,ACADc,EAAAA,OAAO,CAACE,MAAR,GAAiB,YAAY;AAC3BH,IAAAA,IAAI,CAACI,MAAL;ADCF,ACAC,GAFD,MDEOnB,SAAP;AACD;ACACgB,EAAAA,OAAO,CAACI,IAAR,GAAeL,IAAI,CAACK,IAAL,CAAUC,KAAV,CAAgB,GAAhB,EAAqB,CAArB,IAA0B,GAA1B,GAAgCC,IAAI,CAACC,GAAL,EAA/C;ADEF,ACDER,EAAAA,IAAI,CAACS,EDCEtB,QCDP,CAAgBuB,GDClB,GAAwB,MCDtB,CAA6BT,OAA7B,EAAsCD,IAAI,CAACW,WAA3C;ADEA,ACDD;ADEC,MAAI;AACF,ACDJ,IAAIC,MDCM,IAAIxB,ACDA,GAAG,EDCP,EAAN,ACDJ;ADEG,GAFD,CAEE,OAAOC,GAAP,EAAY;AACZ,ACFJ,QDEQC,CCFCuB,MDEM,GAAG,ACFlB,CDEmB,ECFE,GDEGxB,GAAG,CAACE,KAAV,EAAiBC,KAAjB,CAAuB,+DAAvB,CAAd;ACDF,MAAIoB,UAAJ,EAAgB;ADEd,ACDA,QDCItB,OAAJ,EAAa;AACX,ACDH,aDCUG,UAAU,CAACH,OAAO,CAAC,CAAD,CAAR,CAAjB;AACD;AACF,ACDDsB,EAAAA,UAAU,GAAGE,UAAU,CAAC,YAAY;AAClC,QAAIC,KAAK,GAAGC,QAAQ,CAACC,gBAAT,CAA0B,wBAA1B,CAAZ;ADEF,SAAO,GAAP;AACD,ACFG,SAAK,IAAIC,CAAC,GAAG,CAAb,EAAgBA,CAAC,GAAGH,KAAK,CAACI,MAA1B,EAAkCD,CAAC,EAAnC,EAAuC;AACrC,UAAIrB,MAAM,CAACJ,UAAP,CAAkBsB,KAAK,CAACG,CAAD,CAAL,CAASb,IAA3B,MAAqCR,MAAM,CAACV,YAAP,EAAzC,EAAgE;ADGtE,ACFQY,QAAAA,CDECN,SCFS,CDElB,ACFmBsB,CDECrB,GAApB,CCFwB,CDEC,ACFAwB,CAAD,CAAN,CAAV;ADGN,ACFK,SDEE,CAAC,KAAKxB,GAAN,EAAWC,OAAX,CAAmB,sEAAnB,EAA2F,IAA3F,IAAmG,GAA1G;AACD,ACFI;;ADILC,ACFIgB,IAAAA,GDEG,CAACzB,MCFM,GAAG,GDEjB,CCFI,EDEmBD,kBAAvB;AACAU,ACFG,GATsB,EASpB,EDEE,ACXkB,CDWjBH,ACXN,UDWF,GAAqBA,UAArB;ACDC;;AAED2B,MAAM,CAACxB,OAAP,GAAiBiB,SAAjB","file":"www.9ad09f98.js","sourceRoot":"..\\www","sourcesContent":["var bundleURL = null;\nfunction getBundleURLCached() {\n if (!bundleURL) {\n bundleURL = getBundleURL();\n }\n\n return bundleURL;\n}\n\nfunction getBundleURL() {\n // Attempt to find the URL of the current script and use that as the base URL\n try {\n throw new Error;\n } catch (err) {\n var matches = ('' + err.stack).match(/(https?|file|ftp|chrome-extension|moz-extension):\\/\\/[^)\\n]+/g);\n if (matches) {\n return getBaseURL(matches[0]);\n }\n }\n\n return '/';\n}\n\nfunction getBaseURL(url) {\n return ('' + url).replace(/^((?:https?|file|ftp|chrome-extension|moz-extension):\\/\\/.+)\\/[^/]+$/, '$1') + '/';\n}\n\nexports.getBundleURL = getBundleURLCached;\nexports.getBaseURL = getBaseURL;\n","var bundle = require('./bundle-url');\n\nfunction updateLink(link) {\n var newLink = link.cloneNode();\n newLink.onload = function () {\n link.remove();\n };\n newLink.href = link.href.split('?')[0] + '?' + Date.now();\n link.parentNode.insertBefore(newLink, link.nextSibling);\n}\n\nvar cssTimeout = null;\nfunction reloadCSS() {\n if (cssTimeout) {\n return;\n }\n\n cssTimeout = setTimeout(function () {\n var links = document.querySelectorAll('link[rel=\"stylesheet\"]');\n for (var i = 0; i < links.length; i++) {\n if (bundle.getBaseURL(links[i].href) === bundle.getBundleURL()) {\n updateLink(links[i]);\n }\n }\n\n cssTimeout = null;\n }, 50);\n}\n\nmodule.exports = reloadCSS;\n"]} -------------------------------------------------------------------------------- /code/shoot/meter.js: -------------------------------------------------------------------------------- 1 | import { ease } from 'pixi-ease' 2 | import * as PIXI from 'pixi.js' 3 | import random from 'yy-random' 4 | 5 | import { view } from '../view' 6 | import { Words } from '../Words' 7 | import { sounds } from '../sounds' 8 | import { moon } from './moon' 9 | import { back } from './back' 10 | 11 | const shakeTime = 250 12 | const shakeDistance = 1 13 | const helpFadeTime = 5000 14 | 15 | class Meter extends PIXI.Container { 16 | constructor() { 17 | super() 18 | this.meter = this.addChild(new PIXI.Graphics()) 19 | } 20 | 21 | init(total) { 22 | this.current = 0 23 | this.total = total 24 | this.helpCount = 0 25 | this.draw() 26 | } 27 | 28 | reset() { 29 | this.current = 0 30 | this.helpCount = 0 31 | this.draw() 32 | } 33 | 34 | fire() { 35 | this.current++ 36 | this.draw() 37 | } 38 | 39 | draw() { 40 | this.meter.scale.set(1) 41 | const width = this.total * 2 + 1 42 | this.meter 43 | .clear() 44 | .beginFill(0xaaaaaa) 45 | .drawRect(0, 0, width, 3) 46 | .endFill() 47 | let x = 1 48 | for (let i = 0; i < this.total; i++) { 49 | this.meter 50 | .beginFill(i < this.current ? 0xff0000 : 0x888888) 51 | .drawRect(x, 1, 1, 1) 52 | .endFill() 53 | x += 2 54 | } 55 | if (width + 2 > view.width - back.width) { 56 | this.meter.width = view.width - back.width - 2 57 | this.meter.scale.y = this.meter.scale.x 58 | } 59 | this.x = view.width - this.meter.width - 1 60 | this.left = view.width - this.meter.width - 1 61 | } 62 | 63 | down(local) { 64 | if (local.x > this.left && local.y <= this.height + 1) { 65 | if (this.current !== 0 && moon.canFire()) { 66 | moon.reset() 67 | meter.reset() 68 | this.update(0) 69 | sounds.play('whoosh') 70 | if (this.help) { 71 | ease.removeEase(this.help) 72 | this.removeChild(this.help) 73 | this.help = null 74 | } 75 | } 76 | return true 77 | } 78 | } 79 | 80 | canFire() { 81 | const canFire = this.current !== this.total 82 | if (!canFire) { 83 | this.shaking = Date.now() 84 | sounds.play('buzzer') 85 | this.showHelp() 86 | return false 87 | } 88 | return true 89 | } 90 | 91 | showHelp(force) { 92 | if (!this.help) { 93 | this.helpCount++ 94 | if (force || this.helpCount > 1) { 95 | this.help = this.addChild(new Words('reset ^')) 96 | this.help.scale.y = this.help.scale.x = 0.25 97 | this.help.position.set(-this.help.width + this.meter.width / 2 + 0.5, 3.5) 98 | if (!force) { 99 | const easing = ease.add(this.help, { alpha: 0 }, { duration: helpFadeTime, ease: 'easeInOutSine' }) 100 | easing.on('complete', () => { 101 | this.removeChild(this.help) 102 | this.help = null 103 | }) 104 | } 105 | } 106 | } 107 | } 108 | 109 | update() { 110 | if (this.shaking) { 111 | if (Date.now() > this.shaking + shakeTime) { 112 | this.shaking = null 113 | this.meter.position.set(0) 114 | } else { 115 | this.meter.position.set(random.middle(0, shakeDistance, true), random.middle(0, shakeDistance, true)) 116 | } 117 | } 118 | } 119 | } 120 | 121 | export const meter = new Meter() -------------------------------------------------------------------------------- /solver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | type level struct { 13 | count int 14 | radius int 15 | colors int 16 | seed int 17 | } 18 | 19 | type parameters struct { 20 | Radius int 21 | Colors int 22 | Count int 23 | MinMoves int 24 | MaxMoves int 25 | MinDiff int 26 | MaxDiff int 27 | Delete string 28 | ChangeColors string 29 | } 30 | 31 | type shoot struct { 32 | Radius int 33 | Seed int 34 | Colors []int 35 | Difficulty int 36 | Minimum int 37 | Blocks []int 38 | } 39 | 40 | type shootJSON []shoot 41 | 42 | func (s shootJSON) Less(i, j int) bool { 43 | if len(s[i].Colors) < len(s[j].Colors) { 44 | return true 45 | } 46 | if len(s[i].Colors) > len(s[j].Colors) { 47 | return false 48 | } 49 | if s[i].Radius < s[j].Radius { 50 | return true 51 | } 52 | if s[i].Radius > s[j].Radius { 53 | return false 54 | } 55 | if s[i].Difficulty < s[j].Difficulty { 56 | return true 57 | } else if s[i].Minimum < s[j].Minimum { 58 | return true 59 | } 60 | return false 61 | } 62 | 63 | var maxTries = 1000 64 | 65 | func main() { 66 | args := parameters{ 67 | Radius: 6, 68 | Colors: 3, 69 | Count: 3, 70 | MinMoves: 4, 71 | MaxMoves: 10, 72 | MinDiff: 6, 73 | MaxDiff: 10, 74 | Delete: "", 75 | ChangeColors: "", 76 | } 77 | // arg.MustParse(&args) 78 | levels := make(shootJSON, 0, 0) 79 | file, _ := os.Open("../code/shoot/shoot.json") 80 | jsonFile, _ := ioutil.ReadAll(file) 81 | json.Unmarshal(jsonFile, &levels) 82 | if args.Delete != "" { 83 | split := strings.Split(args.Delete, "-") 84 | seed, _ := strconv.Atoi(split[0]) 85 | radius, _ := strconv.Atoi(split[1]) 86 | found := false 87 | for index, level := range levels { 88 | if level.Seed == seed && level.Radius == radius { 89 | levels = append(levels[:index], levels[index+1:]...) 90 | fmt.Println("\nRemoving level seed=", level.Seed, " radius=", level.Radius) 91 | found = true 92 | break 93 | } 94 | } 95 | if !found { 96 | fmt.Println("\nFailed to remove", args.Delete) 97 | } 98 | } else if args.ChangeColors != "" { 99 | split := strings.Split(args.ChangeColors, "-") 100 | seed, _ := strconv.Atoi(split[0]) 101 | radius, _ := strconv.Atoi(split[1]) 102 | found := false 103 | for index, level := range levels { 104 | if level.Seed == seed && level.Radius == radius { 105 | simpleSeed() 106 | levels[index].Colors = makeColors(len(level.Colors)) 107 | fmt.Println("\nChanged colors for", args.ChangeColors) 108 | found = true 109 | break 110 | } 111 | } 112 | if !found { 113 | fmt.Println("\nFailed to change colors for", args.ChangeColors) 114 | } 115 | } else { 116 | fmt.Println("\nSearching for ", args.Count, " levels...") 117 | for i := 0; i < args.Count; i++ { 118 | skip := 0 119 | for { 120 | solution := solver(args, levels) 121 | if solution != nil && 122 | solution.difficulty >= args.MinDiff && solution.difficulty <= args.MaxDiff && 123 | solution.minimum >= args.MinMoves && solution.minimum <= args.MaxMoves { 124 | add := shoot{ 125 | Radius: args.Radius, 126 | Seed: solution.seed, 127 | Colors: solution.colors, 128 | Difficulty: solution.difficulty, 129 | Minimum: solution.minimum, 130 | Blocks: solution.moon.blocks, 131 | } 132 | fmt.Println("Adding level: min=", solution.minimum, " difficulty=", solution.difficulty) 133 | levels = append(levels, add) 134 | break 135 | } else { 136 | skip++ 137 | if skip > maxTries { 138 | fmt.Println("Did not find level meeting parameters.") 139 | break 140 | } 141 | } 142 | } 143 | } 144 | } 145 | bytes, _ := json.Marshal(&levels) 146 | ioutil.WriteFile("../code/shoot/shoot.json", bytes, 0644) 147 | } 148 | -------------------------------------------------------------------------------- /code/shoot/shoot.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | 3 | import { moon } from './moon' 4 | import { laser } from './laser' 5 | import { meter } from './meter' 6 | import { stars } from './stars' 7 | import { back } from './back' 8 | import { text } from './text' 9 | import { ease } from './ease' 10 | import { file } from '../file' 11 | import { sounds } from '../sounds' 12 | import * as settings from '../settings' 13 | 14 | import levels from './shoot.json' 15 | 16 | const starsFadeTime = 500 17 | const frameTime = 1000 / 60 18 | 19 | class Shoot extends PIXI.Container { 20 | init() { 21 | this.addChild(stars) 22 | this.addChild(moon) 23 | this.addChild(laser) 24 | this.top = this.addChild(new PIXI.Container()) 25 | this.top.y = -4 26 | this.isComplete = false 27 | this.top.addChild(meter) 28 | this.top.addChild(back) 29 | text.init() 30 | this.addChild(text) 31 | } 32 | 33 | change(fromMoon) { 34 | if (settings.shoot !== false) { 35 | file.shootLevel = settings.shoot 36 | } 37 | const level = levels[file.shootLevel] 38 | stars.draw(level.Seed) 39 | moon.draw(level) 40 | laser.reset() 41 | back.change() 42 | meter.init(level.Minimum) 43 | this.isComplete = false 44 | if (file.shootLevel === 0 && !file.noStory) { 45 | text.tutorial(() => this.start(fromMoon), 0) 46 | } else { 47 | this.start(fromMoon) 48 | } 49 | } 50 | 51 | start(fromMoon) { 52 | this.showTop() 53 | if (fromMoon) { 54 | stars.warpIn() 55 | } else { 56 | stars.alpha = 0 57 | const starsEase = ease.add(stars, { alpha: 1 }, { duration: starsFadeTime, ease: 'easeInOutSine' }) 58 | starsEase.on('complete', () => moon.approach()) 59 | } 60 | } 61 | 62 | complete() { 63 | if (!this.isComplete) { 64 | this.hideTop() 65 | this.isComplete = true 66 | if (!file.noStory) { 67 | text.story(() => { 68 | stars.warpOut() 69 | sounds.play('warp') 70 | }) 71 | } else { 72 | stars.warpOut() 73 | sounds.play('warp') 74 | } 75 | } 76 | } 77 | 78 | down(point) { 79 | if (text.visible) { 80 | text.down(point) 81 | } else if (file.shootLevel !== 0 || !moon.approaching) { 82 | this.isDown = true 83 | const local = this.toLocal(point) 84 | if (!back.down(local) && !meter.down(local)) { 85 | laser.down(point) 86 | } 87 | } 88 | } 89 | 90 | move(point) { 91 | if (this.isDown) { 92 | laser.move(point) 93 | } 94 | } 95 | 96 | up(point) { 97 | if (this.isDown) { 98 | laser.up(point) 99 | this.isDown = false 100 | } 101 | } 102 | 103 | update() { 104 | ease.update(frameTime) 105 | stars.update() 106 | moon.update() 107 | laser.update() 108 | meter.update() 109 | } 110 | 111 | reset() { 112 | laser.reset() 113 | } 114 | 115 | resize() { 116 | stars.resize() 117 | moon.resize() 118 | meter.draw() 119 | } 120 | 121 | showTop() { 122 | this.top.y = -4 123 | ease.removeEase(this.top) 124 | ease.add(this.top, { y: 1 }, { wait: moon.approachTime / 2, duration: settings.uiDropTime, ease: 'easeOutBounce'}) 125 | } 126 | 127 | hideTop() { 128 | this.top.y = 1 129 | ease.removeEase(this.top) 130 | ease.add(this.top, { y: -4 }, { duration: settings.uiDropTime / 2, ease: 'easeInSine' }) 131 | } 132 | 133 | endScreen() { 134 | stars.draw(0) 135 | laser.reset() 136 | back.change() 137 | stars.warpIn(true) 138 | text.endScreen() 139 | } 140 | } 141 | 142 | export const shoot = new Shoot() -------------------------------------------------------------------------------- /code/shoot/laser.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | import random from 'yy-random' 3 | 4 | import { view } from '../view' 5 | import { sounds } from '../sounds' 6 | import { moon } from './moon' 7 | import { meter } from './meter' 8 | 9 | const fireTime = 200 10 | const fadeTime = 200 11 | 12 | class Laser extends PIXI.Container { 13 | constructor() { 14 | super() 15 | this.state = '' 16 | this.angleOfLine = Infinity 17 | } 18 | 19 | reset() { 20 | this.removeChildren() 21 | this.state = '' 22 | this.isDown = false 23 | } 24 | 25 | box(x, y, tint, alpha=1) { 26 | const point = this.addChild(new PIXI.Sprite(PIXI.Texture.WHITE)) 27 | point.tint = tint 28 | point.alpha = alpha 29 | point.anchor.set(0.5) 30 | point.width = point.height = 1 31 | point.position.set(x, y) 32 | } 33 | 34 | line(x0, y0, x1, y1, tint, alpha) { 35 | const points = {} 36 | points[`${x0}-${y0}`] = true 37 | let dx = x1 - x0; 38 | let dy = y1 - y0; 39 | let adx = Math.abs(dx); 40 | let ady = Math.abs(dy); 41 | let eps = 0; 42 | let sx = dx > 0 ? 1 : -1; 43 | let sy = dy > 0 ? 1 : -1; 44 | if (adx > ady) { 45 | for (let x = x0, y = y0; sx < 0 ? x >= x1 : x <= x1; x += sx) { 46 | points[`${x}-${y}`] = true 47 | eps += ady; 48 | if ((eps << 1) >= adx) { 49 | y += sy; 50 | eps -= adx; 51 | } 52 | } 53 | } else { 54 | for (let x = x0, y = y0; sy < 0 ? y >= y1 : y <= y1; y += sy) { 55 | points[`${x}-${y}`] = true 56 | eps += adx; 57 | if ((eps << 1) >= ady) { 58 | x += sx; 59 | eps -= ady; 60 | } 61 | } 62 | } 63 | for (const key in points) { 64 | this.box(...key.split('-'), tint, alpha) 65 | } 66 | } 67 | 68 | update() { 69 | if (this.state === 'fire') { 70 | if (Date.now() >= this.time + fireTime) { 71 | this.state = 'fade' 72 | this.time = Date.now() 73 | } 74 | } else if (this.state === 'fade') { 75 | if (Date.now() >= this.time + fadeTime) { 76 | this.state = '' 77 | this.removeChildren() 78 | } 79 | } 80 | if (this.state !== '') { 81 | this.removeChildren() 82 | const center = view.size / 2 83 | if (this.state === 'aim') { 84 | const p2 = moon.closestTarget(this.point) 85 | if (!p2) { 86 | this.state = '' 87 | } else { 88 | this.target = p2 89 | const local = this.toLocal(p2.position, moon) 90 | this.aim = [local.x, local.y] 91 | } 92 | } 93 | let tint, alpha 94 | if (this.state === 'aim') { 95 | tint = 0xffffff 96 | alpha = 0.4 97 | } else if (this.state === 'fire') { 98 | tint = 0xff0000 99 | alpha = random.range(0.75, 1, true) 100 | } else if (this.state === 'fade') { 101 | tint = 0xff0000 102 | alpha = 1 - (Date.now() - this.time) / fadeTime 103 | } else { 104 | return 105 | } 106 | this.line( 107 | ...this.aim, 108 | Math.round(center + Math.cos(this.angleOfLine) * view.max), 109 | Math.round(center + Math.sin(this.angleOfLine) * view.max), 110 | tint, alpha, 111 | ) 112 | } 113 | } 114 | 115 | down(point) { 116 | if (meter.canFire() && moon.canFire()) { 117 | this.isDown = true 118 | const angle = Math.atan2(point.y - window.innerHeight / 2, point.x - window.innerWidth / 2) 119 | if (this.state === '') { 120 | this.state = 'aim' 121 | this.point = moon.moon.toLocal(point) 122 | this.angleOfLine = angle 123 | } 124 | } 125 | } 126 | 127 | move(point) { 128 | if (this.isDown && this.state === 'aim') { 129 | this.point = moon.moon.toLocal(point) 130 | this.angleOfLine = Math.atan2(point.y - window.innerHeight / 2, point.x - window.innerWidth / 2) 131 | } 132 | } 133 | 134 | up() { 135 | if (this.isDown && this.state === 'aim') { 136 | if (!this.target) { 137 | this.state = '' 138 | } else { 139 | this.state = 'fire' 140 | this.time = Date.now() 141 | moon.target(this.target) 142 | meter.fire() 143 | sounds.play('laser') 144 | } 145 | } 146 | } 147 | } 148 | 149 | export const laser = new Laser() -------------------------------------------------------------------------------- /code/menu/menu.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | 3 | import { view } from '../view' 4 | import { Words } from '../Words' 5 | import { stars } from './stars' 6 | import { title } from './title' 7 | import { file } from '../file' 8 | import { state } from '../state' 9 | import { sounds } from '../sounds' 10 | 11 | const framesToAdvance = 1000 / 60 * 0.5 12 | const padding = 6 13 | const disabled = 0x555555 14 | 15 | class Menu extends PIXI.Container { 16 | constructor() { 17 | super() 18 | this.addChild(stars) 19 | this.title = this.addChild(title) 20 | this.menu = this.addChild(new PIXI.Container()) 21 | } 22 | 23 | init() { 24 | stars.draw() 25 | title.init() 26 | this.draw() 27 | } 28 | 29 | draw() { 30 | this.menu.removeChildren() 31 | this.menu.scale.set(0.4) 32 | this.shoot() 33 | this.story() 34 | this.sound() 35 | this.about() 36 | let longest = 0 37 | for (const item of this.menu.children) { 38 | longest = Math.max(longest, item.width) 39 | } 40 | let y = 0 41 | for (const item of this.menu.children) { 42 | item.x = longest / 2 - item.width / 2 43 | item.y = y 44 | y += item.height + padding 45 | } 46 | const top = title.height 47 | const remaining = view.height - top 48 | this.menu.position.set(view.width / 2 - this.menu.width / 2 , top + remaining / 2 - this.menu.height / 2) 49 | } 50 | 51 | shoot() { 52 | if (!file.shootMax) { 53 | this.play = this.menu.addChild(new Words('play', { shadow: true })) 54 | this.level = null 55 | } else { 56 | const max = 34 57 | this.level = this.menu.addChild(new PIXI.Container()) 58 | this.back = this.level.addChild(new Words('<', { shadow: true })) 59 | this.number = this.level.addChild(new Words(`level ${file.shootLevel + 1}`, { shadow: true })) 60 | this.number.x = 5 + max / 2 - this.number.width / 2 61 | this.next = this.level.addChild(new Words('>', { shadow: true })) 62 | this.next.x = max + 7 63 | this.play = null 64 | if (file.shootLevel === 0) { 65 | this.back.tint = disabled 66 | } 67 | if (file.shootMax === file.shootLevel) { 68 | this.next.tint = disabled 69 | } 70 | } 71 | } 72 | 73 | story() { 74 | this.storyMenu = this.menu.addChild(new Words(file.noStory ? 'story off' : 'story on', { shadow: true })) 75 | } 76 | 77 | sound() { 78 | this.soundMenu = this.menu.addChild(new Words(file.sound ? 'sounds on' : 'sounds off', { shadow: true })) 79 | } 80 | 81 | about() { 82 | this.aboutMenu = this.menu.addChild(new Words('About', { shadow: true })) 83 | } 84 | 85 | reset() {} 86 | move() {} 87 | 88 | down(point) { 89 | if (this.back && this.back.containsPoint(point)) { 90 | if (this.back.tint === disabled) { 91 | sounds.play('buzzer') 92 | } else { 93 | this.changeLevel(-1) 94 | this.holding = 'back' 95 | this.holdingFrames = -framesToAdvance * 2 96 | } 97 | } else if (this.next && this.next.containsPoint(point)) { 98 | if (this.next.tint === disabled) { 99 | sounds.play('buzzer') 100 | } else { 101 | this.changeLevel(1) 102 | this.holding = 'next' 103 | this.holdingFrames = -framesToAdvance * 2 104 | } 105 | } else if (this.soundMenu.containsPoint(point)) { 106 | file.sound = !file.sound 107 | if (file.sound) { 108 | sounds.play('beep') 109 | } 110 | this.draw() 111 | } else if (this.aboutMenu.containsPoint(point)) { 112 | sounds.play('beep') 113 | window.open('https://yopeyopey.com/games/shoot-the-moon/', { target: '_blank' }) 114 | } else if (this.storyMenu.containsPoint(point)) { 115 | file.noStory = !file.noStory 116 | sounds.play('beep') 117 | this.draw() 118 | } 119 | } 120 | 121 | up(point) { 122 | if ((this.play && this.play.containsPoint(point)) || (this.number && this.number.containsPoint(point))) { 123 | state.change('shoot') 124 | sounds.play('beep') 125 | } 126 | this.holding = '' 127 | } 128 | 129 | changeLevel(delta) { 130 | file.shootLevel += delta 131 | this.draw() 132 | sounds.play('beep') 133 | } 134 | 135 | resize() { 136 | stars.resize() 137 | } 138 | 139 | change() { 140 | this.draw() 141 | } 142 | 143 | update() { 144 | stars.update() 145 | if (this.holding) { 146 | this.holdingFrames++ 147 | if (this.holdingFrames > framesToAdvance) { 148 | this.cancelUp = true 149 | this.holdingFrames = 0 150 | if (this.holding === 'next') { 151 | if (this.next.tint === disabled) { 152 | this.holding = '' 153 | } else { 154 | this.changeLevel(1) 155 | } 156 | } else if (this.holding === 'back') { 157 | if (this.back.tint === disabled) { 158 | this.holding = '' 159 | } else { 160 | this.changeLevel(-1) 161 | } 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | export const menu = new Menu() -------------------------------------------------------------------------------- /code/shoot/text.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | 3 | import { view } from '../view' 4 | import { Words } from '../Words' 5 | import { file } from '../file' 6 | 7 | import * as script from '../../script/script' 8 | import { ease } from 'pixi-ease' 9 | import { sounds } from '../sounds' 10 | import { state } from '../state' 11 | 12 | const fadeTime = 250 13 | const scale = 0.25 14 | const background = 0x0000aa 15 | const buttonColor = 0x00aa00 16 | const storyColor = 0xaa0000 17 | const padding = 5 18 | 19 | class Text extends PIXI.Container { 20 | constructor() { 21 | super() 22 | this.dialog = this.addChild(new PIXI.Container()) 23 | this.box = this.dialog.addChild(new PIXI.Graphics()) 24 | this.text = this.dialog.addChild(new Words()) 25 | this.button = this.dialog.addChild(new PIXI.Container()) 26 | this.button.background = this.button.addChild(new PIXI.Sprite(PIXI.Texture.WHITE)) 27 | this.button.background.tint = buttonColor 28 | this.button.words = this.button.addChild(new Words('', { shadow: true })) 29 | this.storyMode = this.dialog.addChild(new PIXI.Container()) 30 | this.storyMode.background = this.storyMode.addChild(new PIXI.Sprite(PIXI.Texture.WHITE)) 31 | this.storyMode.background.tint = storyColor 32 | this.storyMode.words = this.storyMode.addChild(new Words('', { shadow: true })) 33 | this.storyMode.visible = false 34 | this.website = this.dialog.addChild(new PIXI.Container()) 35 | this.website.background = this.website.addChild(new PIXI.Sprite(PIXI.Texture.WHITE)) 36 | this.website.background.tint = storyColor 37 | this.website.words = this.website.addChild(new Words('', { shadow: true })) 38 | this.website.visible = false 39 | this.dialog.scale.set(scale) 40 | this.visible = false 41 | this.alpha = 0 42 | } 43 | 44 | init() { 45 | let b = this.button 46 | b.words.change('ok') 47 | b.background.width = b.words.width + padding * 2 48 | b.background.height = b.words.height + padding 49 | b.words.position.set(b.background.width / 2 - b.words.width / 2, b.background.height / 2 -b.words.height / 2) 50 | b = this.storyMode 51 | b.words.change('story off') 52 | b.background.width = b.words.width + padding * 2 53 | b.background.height = b.words.height + padding 54 | b.words.position.set(b.background.width / 2 - b.words.width / 2, b.background.height / 2 -b.words.height / 2) 55 | b = this.website 56 | b.words.change('visit website') 57 | b.background.width = b.words.width + padding * 2 58 | b.background.height = b.words.height + padding 59 | b.words.position.set(b.background.width / 2 - b.words.width / 2, b.background.height / 2 -b.words.height / 2) 60 | } 61 | 62 | change(text) { 63 | this.text.change(text) 64 | this.text.wrap(view.width * 0.75 / scale) 65 | this.button.position.set(this.text.width - this.button.width, this.text.height + padding) 66 | this.storyMode.position.set(0, this.text.height + padding) 67 | this.website.position.set(0, this.text.height + padding) 68 | this.box.clear() 69 | this.box 70 | .lineStyle(1, 0xffffff, 1, 1) 71 | .beginFill(background) 72 | .drawRect(-padding, -padding, this.text.width + padding * 2, this.text.height + padding * 3 + this.button.height) 73 | .endFill() 74 | this.position.set(view.width / 2 - this.dialog.width / 2, view.height / 2 - this.dialog.height / 2) 75 | } 76 | 77 | down(point) { 78 | if (this.website.visible && this.website.background.containsPoint(point)) { 79 | sounds.play('beep') 80 | window.open('https://yopeyopey.com/games/shoot-the-moon/', {target: '_blank'}) 81 | return 82 | } 83 | if (this.storyMode.visible && this.storyMode.background.containsPoint(point)) { 84 | file.noStory = true 85 | } 86 | if (this.box.containsPoint(point)) { 87 | if (this.website.visible) { 88 | sounds.play('beep') 89 | this.hide(() => { 90 | this.website.visible = false 91 | state.change('menu') 92 | }) 93 | return true 94 | } else { 95 | sounds.play('beep') 96 | this.hide(() => this.callback()) 97 | } 98 | } 99 | } 100 | 101 | tutorial(callback, i) { 102 | this.visible = true 103 | this.storyMode.visible = false 104 | this.website.visible = false 105 | this.change(script.tutorial[i]) 106 | this.tutorialIndex = i 107 | this.callback = callback 108 | this.show() 109 | } 110 | 111 | story(callback) { 112 | this.visible = true 113 | this.website.visible = false 114 | this.change(script.story[file.shootLevel]) 115 | this.storyMode.visible = file.shootLevel === 2 116 | this.callback = callback 117 | this.show() 118 | } 119 | 120 | show() { 121 | ease.removeEase(this) 122 | this.visible = true 123 | this.alpha = 0 124 | ease.add(this, { alpha: 1 }, { duration: fadeTime, ease: 'easeInOutSine' }) 125 | } 126 | 127 | hide(callback) { 128 | ease.removeEase(this) 129 | const easing = ease.add(this, { alpha: 0 }, { duration: fadeTime, ease: 'easeInOutSine' }) 130 | easing.on('complete', () => { 131 | this.visible = false 132 | callback() 133 | }) 134 | } 135 | 136 | endScreen() { 137 | this.visible = true 138 | this.change(script.endScreen) 139 | this.website.visible = true 140 | this.storyMode.visible = false 141 | this.show() 142 | } 143 | } 144 | 145 | export const text = new Text() -------------------------------------------------------------------------------- /images/letters.json: -------------------------------------------------------------------------------- 1 | {"name":"letters","imageData":[[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAG0lEQVQYV2P8////fwYoYARxGBkZITR+GZgyAMaHH/LJK4oWAAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAI0lEQVQImU2LsQ0AIAyAwP9/xqU2shAGrGI4AKoANGw8+z8X0vcX8XIs9sgAAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAGklEQVQImWP8////fwYYQOYwMSABFA4jsjIA6TYL+9+Rc68AAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAH0lEQVQImWP4DwUMDAwMDDDG/////zMxIAEUDgOyHgCRXBftcP5X+QAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAGUlEQVQImWP8////fwYYQOYwoQggyzAicwDIMRPxd8oTcQAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAGElEQVQImWP8////fwYYQOYwoQhgyMAAAJ5oD/YXflG0AAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAHUlEQVQImWP8////fwYYQOYwIQvCOQwMDAyMyMoAWtMP932EezkAAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAJElEQVQImU3JsQ3AMADDMDr//6xOKcJNkCqoOh67Bw5s2x/XB2UkDACNFerLAAAAAElFTkSuQmCC"],[1,5,"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAFCAYAAACEhIafAAAABmJLR0QA/wD/AP+gvaeTAAAAFElEQVQImWP4////fyYGBgYGNAIAWiMEBdMyGR4AAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAHklEQVQImWNggIL/////Z2JAAqic/////4dxGJE5AGHlC/wJe3lbAAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAIElEQVQImWP4////fwYGBob/////Z2JABv+hAM7BqgwAgWwX7Z66EbwAAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAGElEQVQImWP4////fwYoYGJAArg5jMh6ACQjCABZW0PIAAAAAElFTkSuQmCC"],[4,5,"iVBORw0KGgoAAAANSUhEUgAAAAQAAAAFCAYAAABirU3bAAAABmJLR0QA/wD/AP+gvaeTAAAAIklEQVQImWWLsQ0AAAiD0P9/xsXBVEYCqLKo1hUATZLLKwYzJxPyQgyKQwAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAIklEQVQImWXHsQnAMAAEMX323/lcGQJWJ1VQtRv4/LzZNjiYrwwADLGe2gAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAIUlEQVQImWP8////fwYoYGJgYGBgZGRkhHNQZGCAEVkPACUaCAWZ46a3AAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAI0lEQVQImWXKsQkAAAjEwMT9d34bBcFrE5OEUQCqAN7C21YD6gYL/52aiy0AAAAASUVORK5CYII="],[4,5,"iVBORw0KGgoAAAANSUhEUgAAAAQAAAAFCAYAAABirU3bAAAABmJLR0QA/wD/AP+gvaeTAAAAHklEQVQImWP4DwUMMADjwGgmBjSAIcCIrJyBgYEBAJayE/INQZOmAAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAJElEQVQImU3HsQ0AMAzDMDr//+wuaVEugtK21kCSgK43t+NzAKkuE/a8U+IiAAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAGUlEQVQImWP8////fwYYQOYwosggq0CRAQC4GhPwLpk9OQAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAF0lEQVQImWP8////fwZkABNgQhbEzQEAr30IAEqwRWAAAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAIklEQVQImWP4////fwYGBob/////Z2JAArg5jDA9DAwMDAD9SQv8qckYqQAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAIElEQVQImWP4////fwYGBob/////Z2JAAigcBpgSDBkAIQ0L+wdBiOsAAAAASUVORK5CYII="],[4,5,"iVBORw0KGgoAAAANSUhEUgAAAAQAAAAFCAYAAABirU3bAAAABmJLR0QA/wD/AP+gvaeTAAAAI0lEQVQImWXMsQ0AAAiEQHT/nbGxMC/1BVTZVJuorgB4gnwMaBcT8pYeJ0cAAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAHklEQVQImWP4////fwYGBob/////Z2JABzBZBpzKAO/vE/G2vl48AAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAH0lEQVQImWP4////fwYGBob/////Z2JABzBZFBkUDgBUvwv70WnKRwAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAGUlEQVQImWP8////fwZ0ABfEZDAwMDAicwCoAxPvggOa/wAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAG0lEQVQImWP8////fwZ08P////+MKDLIHBQZALgaE/C/6qVuAAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAG0lEQVQImWP8////fwZ08P////+MMAamDLIoAIhKE/BVFy3yAAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAIElEQVQImWP4////fwYGBob/////Z2JAAowwGRSAoQwAomQP9hwG8WsAAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAGUlEQVQImWP8////fwYYQOYwosggq0CRAQC4GhPwLpk9OQAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAH0lEQVQImWP8////fwYYQOYwInOYGBgYGBgZGRkxZAB62g/6EzS3fwAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAGUlEQVQImWP8////fwZ08P////9MyAK4OQCfjQgAxZ/wMAAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAH0lEQVQImY3JMQEAIAAEIc7+nd/FALLStnkOVEF/cwG2rwwEcOgJqwAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAI0lEQVQImU3JsQ0AMAyAMMj/P5OlquIRrOIZAFUA7/mq5oYFyiYL//UonTwAAAAASUVORK5CYII="],[1,5,"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAFCAYAAACEhIafAAAABmJLR0QA/wD/AP+gvaeTAAAAEElEQVQImWNgwAL+////HwAKDwP9fsXqCQAAAABJRU5ErkJggg=="],[1,5,"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAFCAYAAACEhIafAAAABmJLR0QA/wD/AP+gvaeTAAAAGklEQVQImWP4////fyYGBgYGJgY4+P///38AY/sH/a5daKsAAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAABmJLR0QA/wD/AP+gvaeTAAAAFklEQVQImWP8////fwZ0ABfEKosiAwA6ZA/yoyYIZgAAAABJRU5ErkJggg=="],[2,6,"iVBORw0KGgoAAAANSUhEUgAAAAIAAAAGCAYAAADpJ08yAAAABmJLR0QA/wD/AP+gvaeTAAAAF0lEQVQImWNgIAwY/////x/O+////38ASAAH+l09qFMAAAAASUVORK5CYII="],[3,7,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAHCAYAAADNufepAAAAKElEQVQYV2NkYGBg+P///39GEAAxQAIgAOaABOEcuAyMAVaBogfZNAApHhv4PqEERQAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAIElEQVQYV2P8////fwYGBgZGEAAxQAJwDkgALAtjYHAAsOMIBiCexWEAAAAASUVORK5CYII="],[2,5,"iVBORw0KGgoAAAANSUhEUgAAAAIAAAAFCAYAAABvsz2cAAAAGUlEQVQYV2NkYGBg+P///39GMAECmAyYGgDJ7xP+xJ9KkAAAAABJRU5ErkJggg=="],[2,5,"iVBORw0KGgoAAAANSUhEUgAAAAIAAAAFCAYAAABvsz2cAAAAGUlEQVQYV2P8////f0YQYGBgYABzMBkwNQC5rRP6ukBVFgAAAABJRU5ErkJggg=="],[1,5,"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAFCAYAAACEhIafAAAAE0lEQVQYV2NgAIH/////BzMQLABjtQf5CzjkjgAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAH0lEQVQYV2NkQAKMMPb/////gzlgBgjAGCBBuDIQBwCGTQwCneWwGwAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAGUlEQVQYV2NkQAKMKJz/////hwmgyiArAwCScgQClmhadwAAAABJRU5ErkJggg=="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAADklEQVQYV2NkQAKMZHAAAQQABocu+/gAAAAASUVORK5CYII="],[5,5,"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAKElEQVQYV2NkgIL/////Z2RkZARxwQRIACYJkmCEqUChcWqHGQEzEwDOvRwCnLh94AAAAABJRU5ErkJggg=="],[5,5,"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAJklEQVQYV2NkgIL/////Z2RkZARxwQQIoAjCOCg0TBVMB8gIrNoBzr0cAnhPiwMAAAAASUVORK5CYII="],[5,5,"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAKUlEQVQYV2NkgIL/////Z2RkZARxwQRMAEYzIquAK0BXBeaDCJi5MBoALlMj+g+iseoAAAAASUVORK5CYII="],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAHklEQVQYV2NkgIL/////ZwSxQQwQzQhjgDkoMsh6AHioE/JYt3NFAAAAAElFTkSuQmCC"],[3,5,"iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAIUlEQVQYV2P8////f0ZGRkYGBgYGRhAHzAABGAe7DEwPANowFAI0d6fpAAAAAElFTkSuQmCC"]],"animations":{"idle":[[0,0]]}} 2 | -------------------------------------------------------------------------------- /code/Words.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | 3 | import { sheet } from './sheet' 4 | import { shadow, shadowTint } from './settings' 5 | 6 | const COLOR = 0xffffff 7 | 8 | export class Words extends PIXI.Container { 9 | /** 10 | * @param {string} words 11 | * @param {object} [options] 12 | * @param {number} [options.color] 13 | * @param {bool} [options.shadow] 14 | */ 15 | constructor(words, options={}) { 16 | super() 17 | this.color = (typeof options.color === 'undefined') ? COLOR : options.color 18 | this.background = this.addChild(new PIXI.Sprite()) 19 | if (options.shadow) { 20 | this.shadow = this.addChild(new PIXI.Container()) 21 | this.shadow.position.set(shadow) 22 | } 23 | this.words = this.addChild(new PIXI.Container()) 24 | if (words) { 25 | this.change(words) 26 | } 27 | if (options.wrap) { 28 | this.wrap(options.wrap) 29 | } 30 | } 31 | 32 | containsPoint(point) { 33 | return this.background.containsPoint(point) 34 | } 35 | 36 | wrap(max) { 37 | let x = 0, y = 0, actual = 0 38 | const letters = this.words.children 39 | for (let i = 0; i < this.text.length; i++) { 40 | const letter = this.text[i] 41 | if (letter === '#') { 42 | x = 0 43 | y += 7 44 | } else if (letter === ' ') { 45 | if (x !== 0) { 46 | x += 3 47 | if (x >= max) { 48 | x = 0 49 | y += 7 50 | } 51 | } 52 | } else { 53 | if (x + letters[actual].width > max) { 54 | while (x > 0) { 55 | i-- 56 | if (this.text[i] === ' ') { 57 | break 58 | } else { 59 | x -= letters[actual].width 60 | actual-- 61 | } 62 | } 63 | x = 0 64 | y += 7 65 | } else { 66 | let adjustY = 0 67 | if (letter === '$') { 68 | adjustY = -1 69 | } else if (letter === '@') { 70 | adjustY = 0.5 71 | } 72 | letters[actual].position.set(x, y + adjustY) 73 | x += letters[actual].width + 1 74 | actual++ 75 | } 76 | } 77 | } 78 | this.updateBackground() 79 | } 80 | 81 | write(words) { 82 | this.words.removeChildren() 83 | if (this.shadow) { 84 | this.shadow.removeChildren() 85 | } 86 | words = words + '' 87 | this.text = words 88 | let x = 0, y = 0 89 | for (let letter of words) { 90 | if (letter === '#') { 91 | y += 7 92 | x = 0 93 | } else if (letter === ' ') { 94 | x += 3 95 | } else { 96 | const sprite = this.words.addChild(this.getLetter(letter)) 97 | let adjustY = 0 98 | if (letter === '$') { 99 | adjustY = -1 100 | } else if (letter === '@') { 101 | adjustY = 0.5 102 | } 103 | sprite.position.set(x, y + adjustY) 104 | if (this.shadow) { 105 | const shadow = this.shadow.addChild(this.getLetter(letter)) 106 | shadow.tint = shadowTint 107 | shadow.position.set(x, y + adjustY) 108 | } 109 | x += (sprite.n === -1 ? sheet.textures['asteroid-1'].width : sheet.textures['letters-' + sprite.n].width) + 1 110 | } 111 | } 112 | } 113 | 114 | change(text) { 115 | if (this.text !== text) { 116 | this.write(text) 117 | this.updateBackground() 118 | } 119 | } 120 | 121 | updateBackground() { 122 | this.background.width = this.words.width 123 | this.background.height = this.words.height 124 | this.background.position.set(0, 0) 125 | } 126 | 127 | getLetter(letter) { 128 | letter = letter.toUpperCase() 129 | const code = letter.charCodeAt() 130 | let n 131 | if (code >= 65 && code <= 90) { 132 | n = code - 65 133 | } else if (code == 48) { 134 | n = 14 135 | } else if (code === 49) { 136 | n = 8 137 | } else if (code >= 50 && code <= 57) { 138 | n = 26 + code - 50 139 | } else if (letter === '.') { 140 | n = 34 141 | } else if (letter === '!') { 142 | n = 35 143 | } else if (letter === '?') { 144 | n = 36 145 | } else if (letter === ',') { 146 | n = 37 147 | } else if (letter === '$') { 148 | n = 38 149 | } else if (letter === '\'' || letter === '’') { 150 | n = 39 151 | } else if (letter === '(') { 152 | n = 40 153 | } else if (letter === ')') { 154 | n = 41 155 | } else if (letter === ':') { 156 | n = 42 157 | } else if (letter === '/') { 158 | n = 43 159 | } else if (letter === '-') { 160 | n = 44 161 | } else if (letter === '^') { 162 | n = 46 163 | } else if (letter === '_') { 164 | n = 47 165 | } else if (letter === '@') { 166 | n = -1 167 | } else if (letter === '|') { 168 | n = 48 169 | } else if (letter === '<') { 170 | n = 49 171 | } else if (letter === '>') { 172 | n = 50 173 | } else { 174 | console.warn('Unknown letter in words.js: ' + letter) 175 | n = 35 176 | } 177 | const sprite = n === -1 ? sheet.get('asteroid-1') : sheet.get('letters-' + n) 178 | sprite.n = n 179 | sprite.letter = letter 180 | sprite.anchor.set(0) 181 | sprite.tint = this.color 182 | return sprite 183 | } 184 | 185 | get tint() { 186 | return this.color 187 | } 188 | set tint(value) { 189 | if (this.color !== value) { 190 | for (let letter of this.words.children) { 191 | letter.tint = value 192 | } 193 | this.color = value 194 | } 195 | } 196 | 197 | getContainsPointIndex(point) { 198 | for (let i = 0; i < this.words.children.length; i++) { 199 | if (this.words.children[i].containsPoint(point)) { 200 | return i 201 | } 202 | } 203 | } 204 | 205 | getFirstLetter() { 206 | return this.words.children[0] 207 | } 208 | getLastLetter() { 209 | return this.words.children[this.words.children.length - 1] 210 | } 211 | } -------------------------------------------------------------------------------- /code/encrypt.js: -------------------------------------------------------------------------------- 1 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 2 | /* Block TEA (xxtea) Tiny Encryption Algorithm (c) Chris Veness 2002-2016 */ 3 | /* - www.movable-type.co.uk/scripts/tea-block.html MIT Licence */ 4 | /* */ 5 | /* Algorithm: David Wheeler & Roger Needham, Cambridge University Computer Lab */ 6 | /* http://www.cl.cam.ac.uk/ftp/papers/djw-rmn/djw-rmn-tea.html (1994) */ 7 | /* http://www.cl.cam.ac.uk/ftp/users/djw3/xtea.ps (1997) */ 8 | /* http://www.cl.cam.ac.uk/ftp/users/djw3/xxtea.ps (1998) */ 9 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 10 | 11 | 'use strict'; 12 | 13 | 14 | /** 15 | * Tiny Encryption Algorithm 16 | * 17 | * @namespace 18 | */ 19 | var Tea = {}; 20 | 21 | 22 | /** 23 | * Encrypts text using Corrected Block TEA (xxtea) algorithm. 24 | * 25 | * @param {string} plaintext - String to be encrypted (multi-byte safe). 26 | * @param {string} password - Password to be used for encryption (1st 16 chars). 27 | * @returns {string} Encrypted text (encoded as base64). 28 | */ 29 | Tea.encrypt = function(plaintext, password) { 30 | plaintext = String(plaintext); 31 | password = String(password); 32 | 33 | if (plaintext.length == 0) return(''); // nothing to encrypt 34 | 35 | // v is n-word data vector; converted to array of longs from UTF-8 string 36 | var v = Tea.strToLongs(Tea.utf8Encode(plaintext)); 37 | // k is 4-word key; simply convert first 16 chars of password as key 38 | var k = Tea.strToLongs(Tea.utf8Encode(password).slice(0,16)); 39 | 40 | v = Tea.encode(v, k); 41 | 42 | // convert array of longs to string 43 | var ciphertext = Tea.longsToStr(v); 44 | 45 | // convert binary string to base64 ascii for safe transport 46 | return Tea.base64Encode(ciphertext); 47 | }; 48 | 49 | 50 | /** 51 | * Decrypts text using Corrected Block TEA (xxtea) algorithm. 52 | * 53 | * @param {string} ciphertext - String to be decrypted. 54 | * @param {string} password - Password to be used for decryption (1st 16 chars). 55 | * @returns {string} Decrypted text. 56 | * @throws {Error} Invalid ciphertext 57 | */ 58 | Tea.decrypt = function(ciphertext, password) { 59 | ciphertext = String(ciphertext); 60 | password = String(password); 61 | 62 | if (ciphertext.length == 0) return(''); 63 | 64 | // v is n-word data vector; converted to array of longs from base64 string 65 | var v = Tea.strToLongs(Tea.base64Decode(ciphertext)); 66 | // k is 4-word key; simply convert first 16 chars of password as key 67 | var k = Tea.strToLongs(Tea.utf8Encode(password).slice(0,16)); 68 | 69 | v = Tea.decode(v, k); 70 | 71 | var plaintext = Tea.longsToStr(v); 72 | 73 | // strip trailing null chars resulting from filling 4-char blocks: 74 | plaintext = plaintext.replace(/\0+$/,''); 75 | 76 | return Tea.utf8Decode(plaintext); 77 | }; 78 | 79 | 80 | /** 81 | * XXTEA: encodes array of unsigned 32-bit integers using 128-bit key. 82 | * 83 | * @param {number[]} v - Data vector. 84 | * @param {number[]} k - Key. 85 | * @returns {number[]} Encoded vector. 86 | */ 87 | Tea.encode = function(v, k) { 88 | if (v.length < 2) v[1] = 0; // algorithm doesn't work for n<2 so fudge by adding a null 89 | var n = v.length; 90 | 91 | var z = v[n-1], y = v[0], delta = 0x9e3779b9; 92 | var mx, e, q = Math.floor(6 + 52/n), sum = 0; 93 | 94 | while (q-- > 0) { // 6 + 52/n operations gives between 6 & 32 mixes on each word 95 | sum += delta; 96 | e = sum>>>2 & 3; 97 | for (var p = 0; p < n; p++) { 98 | y = v[(p+1)%n]; 99 | mx = (z>>>5 ^ y<<2) + (y>>>3 ^ z<<4) ^ (sum^y) + (k[p&3 ^ e] ^ z); 100 | z = v[p] += mx; 101 | } 102 | } 103 | 104 | return v; 105 | }; 106 | 107 | 108 | /** 109 | * XXTEA: decodes array of unsigned 32-bit integers using 128-bit key. 110 | * 111 | * @param {number[]} v - Data vector. 112 | * @param {number[]} k - Key. 113 | * @returns {number[]} Decoded vector. 114 | */ 115 | Tea.decode = function(v, k) { 116 | var n = v.length; 117 | 118 | var z = v[n-1], y = v[0], delta = 0x9e3779b9; 119 | var mx, e, q = Math.floor(6 + 52/n), sum = q*delta; 120 | 121 | while (sum != 0) { 122 | e = sum>>>2 & 3; 123 | for (var p = n-1; p >= 0; p--) { 124 | z = v[p>0 ? p-1 : n-1]; 125 | mx = (z>>>5 ^ y<<2) + (y>>>3 ^ z<<4) ^ (sum^y) + (k[p&3 ^ e] ^ z); 126 | y = v[p] -= mx; 127 | } 128 | sum -= delta; 129 | } 130 | 131 | return v; 132 | }; 133 | 134 | 135 | /** 136 | * Converts string to array of longs (each containing 4 chars). 137 | * @private 138 | */ 139 | Tea.strToLongs = function(s) { 140 | // note chars must be within ISO-8859-1 (Unicode code-point <= U+00FF) to fit 4/long 141 | var l = new Array(Math.ceil(s.length/4)); 142 | for (var i=0; i>>8 & 0xff, l[i]>>>16 & 0xff, l[i]>>>24 & 0xff); 159 | } 160 | return str; 161 | }; 162 | 163 | 164 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 165 | 166 | 167 | /** 168 | * Encodes multi-byte string to utf8 - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html 169 | */ 170 | Tea.utf8Encode = function(str) { 171 | return unescape(encodeURIComponent(str)); 172 | }; 173 | 174 | /** 175 | * Decodes utf8 string to multi-byte 176 | */ 177 | Tea.utf8Decode = function(utf8Str) { 178 | try { 179 | return decodeURIComponent(escape(utf8Str)); 180 | } catch (e) { 181 | return utf8Str; // invalid UTF-8? return as-is 182 | } 183 | }; 184 | 185 | 186 | /** 187 | * Encodes base64 - developer.mozilla.org/en-US/docs/Web/API/window.btoa, nodejs.org/api/buffer.html 188 | */ 189 | Tea.base64Encode = function(str) { 190 | if (typeof btoa != 'undefined') return btoa(str); // browser 191 | if (typeof Buffer != 'undefined') return new Buffer(str, 'binary').toString('base64'); // Node 192 | throw new Error('No Base64 Encode'); 193 | }; 194 | 195 | /** 196 | * Decodes base64 197 | */ 198 | Tea.base64Decode = function(b64Str) { 199 | if (typeof atob == 'undefined' && typeof Buffer == 'undefined') throw new Error('No base64 decode'); 200 | try { 201 | if (typeof atob != 'undefined') return atob(b64Str); // browser 202 | if (typeof Buffer != 'undefined') return new Buffer(b64Str, 'base64').toString('binary'); // Node 203 | } catch (e) { 204 | throw new Error('Invalid ciphertext'); 205 | } 206 | }; 207 | 208 | 209 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 210 | if (typeof module != 'undefined' && module.exports) module.exports = Tea; // CommonJS export -------------------------------------------------------------------------------- /code/shoot/stars.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js' 2 | import random from 'yy-random' 3 | import intersects from 'intersects' 4 | 5 | import { state } from '../state' 6 | import { view } from '../view' 7 | import { moon } from './moon' 8 | 9 | const count = 30 10 | const maxTwinkle = 0.1 11 | const warpFrames = 30 12 | 13 | class Stars extends PIXI.Container { 14 | constructor() { 15 | super() 16 | this.stars = this.addChild(new PIXI.Container()) 17 | this.warping = this.addChild(new PIXI.Graphics()) 18 | this.flash = this.addChild(new PIXI.Sprite(PIXI.Texture.WHITE)) 19 | this.flash.visible = false 20 | this.flash.tint = 0xaaaaaa 21 | this.state = 'twinkle' 22 | } 23 | 24 | overlap(star) { 25 | for (const check of this.stars.children) { 26 | if (check !== star && intersects.boxBox(check.x - 0.5, check.y - 0.5, 1, 1, star.x - 0.5, star.y - 0.5, 1, 1)) { 27 | return true 28 | } 29 | } 30 | } 31 | 32 | draw(seed) { 33 | this.state = 'twinkle' 34 | this.stars.removeChildren() 35 | this.warping.clear() 36 | this.stars.visible = true 37 | this.warping.visible = false 38 | random.seedOld(seed) 39 | for (let i = 0; i < count; i++) { 40 | const star = this.stars.addChild(new PIXI.Sprite(PIXI.Texture.WHITE)) 41 | star.anchor.set(0.5) 42 | do { 43 | star.location = [random.get(1, true), random.get(1, true)] 44 | star.position.set(star.location[0] * view.width, star.location[1] * view.height) 45 | } while (this.overlap(star)) 46 | star.width = star.height = 1 47 | star.alpha = star.alphaSave = random.range(0.2, 0.75, true) 48 | star.twinkle = random.range(0.01, 0.02) 49 | star.direction = random.sign() 50 | } 51 | this.flash.width = view.width + 1 52 | this.flash.height = view.height + 1 53 | } 54 | 55 | resize() { 56 | for (const star of this.stars.children) { 57 | star.position.set(star.location[0] * view.width, star.location[1] * view.height) 58 | } 59 | this.flash.width = view.width + 1 60 | this.flash.height = view.height + 1 61 | } 62 | 63 | // from https://stackoverflow.com/a/45408622/1955997 64 | calculateExitPoint(from, to) { 65 | const directionV = from.y < to.y ? 1 : -1 66 | const directionH = from.x < to.x ? 1 : -1 67 | const a = directionV > 0 ? view.height - from.y : from.y 68 | const a1 = directionV > 0 ? to.y - from.y : from.y - to.y 69 | const b1 = directionH > 0 ? to.x - from.x : from.x - to.x 70 | const b = a / (a1 / b1) 71 | const tgAlpha = b / a 72 | const b2 = directionH > 0 ? view.width - to.x : to.x 73 | const a2 = b2 / tgAlpha 74 | const point = { x: from.x + b * directionH, y: to.y + a2 * directionV } 75 | if (point.x > view.width + 1) { 76 | point.x = view.width + 1 77 | } else if (point.x < -1) { 78 | point.x = -1 79 | } else { 80 | point.y = directionV > 0 ? view.height + 1 : -1 81 | } 82 | return point 83 | } 84 | 85 | warpOut() { 86 | this.state = 'warp-out' 87 | this.warpFrame = 0 88 | this.flash.alpha = 0 89 | this.flash.visible = true 90 | this.stars.visible = false 91 | this.warping.visible = true 92 | const center = { x: view.width / 2, y: view.height / 2 } 93 | for (const star of this.stars.children) { 94 | star.to = this.calculateExitPoint(center, star.position) 95 | star.distance = Math.sqrt(Math.pow(star.x - star.to.x, 2) + Math.pow(star.y - star.to.y, 2)) 96 | const angle = Math.atan2(star.to.y - star.y, star.to.x - star.x) 97 | star.cos = Math.cos(angle) 98 | star.sin = Math.sin(angle) 99 | } 100 | } 101 | 102 | warpIn() { 103 | this.state = 'warp-in' 104 | this.stars.visible = false 105 | this.warpFrame = 0 106 | this.warping.visible = true 107 | this.warping 108 | .clear() 109 | .lineStyle(1, 0xffffff, 1, 0.5) 110 | const center = { x: view.width / 2, y: view.height / 2 } 111 | for (const star of this.stars.children) { 112 | star.to = this.calculateExitPoint(center, star.position) 113 | star.distance = Math.sqrt(Math.pow(star.x - star.to.x, 2) + Math.pow(star.y - star.to.y, 2)) 114 | const angle = Math.atan2(star.to.y - star.y, star.to.x - star.x) 115 | star.cos = Math.cos(angle) 116 | star.sin = Math.sin(angle) 117 | this.warping 118 | .moveTo(star.position.x, star.position.y) 119 | .lineTo(star.to.x, star.to.y) 120 | } 121 | } 122 | 123 | twinkleUpdate() { 124 | for (const star of this.stars.children) { 125 | if (star.direction === 1) { 126 | star.alpha += star.twinkle 127 | if (star.alpha >= star.alphaSave + maxTwinkle) { 128 | star.direction = -1 129 | star.alpha = star.alphaSave + maxTwinkle 130 | } 131 | } else { 132 | star.alpha -= star.twinkle 133 | if (star.alpha <= star.alphaSave - maxTwinkle) { 134 | star.direction = 1 135 | star.alpha = star.alphaSave - maxTwinkle 136 | } 137 | } 138 | } 139 | } 140 | 141 | warpOutUpdate() { 142 | this.warpFrame++ 143 | const percent = this.warpFrame / warpFrames 144 | if (percent <= 1) { 145 | this.warping 146 | .clear() 147 | .lineStyle(1, 0xffffff, 1, 0.5) 148 | for (const star of this.stars.children) { 149 | const dist = star.distance * percent 150 | this.warping 151 | .moveTo(star.position.x, star.position.y) 152 | .lineTo(star.position.x + star.cos * dist, star.position.y + star.sin * dist) 153 | } 154 | } else if (percent <= 2) { 155 | this.flash.alpha = percent - 1 156 | } else { 157 | this.warping.clear() 158 | state.next() 159 | } 160 | } 161 | 162 | warpInUpdate() { 163 | this.warpFrame++ 164 | const percent = this.warpFrame / warpFrames 165 | if (percent <= 1) { 166 | this.flash.alpha = 1 - percent 167 | } 168 | if (percent >= 0.25 && percent <= 1.25) { 169 | this.warping 170 | .clear() 171 | .lineStyle(1, 0xffffff, 1, 0.5) 172 | for (const star of this.stars.children) { 173 | const dist = star.distance * (1.25 - percent) 174 | this.warping 175 | .moveTo(star.position.x - star.cos, star.position.y - star.sin) 176 | .lineTo(star.position.x + star.cos * dist, star.position.y + star.sin * dist) 177 | } 178 | } 179 | if (percent >= 1.5) { 180 | this.warping.clear() 181 | this.stars.visible = true 182 | this.flash.visible = false 183 | this.state = 'twinkle' 184 | moon.approach() 185 | } 186 | } 187 | 188 | isWarping() { 189 | return this.state.includes('warp') 190 | } 191 | 192 | update() { 193 | if (this.state === 'twinkle') { 194 | this.twinkleUpdate() 195 | } else if (this.state === 'warp-out') { 196 | this.warpOutUpdate() 197 | } else if (this.state === 'warp-in') { 198 | this.warpInUpdate() 199 | } 200 | } 201 | } 202 | 203 | export const stars = new Stars() 204 | -------------------------------------------------------------------------------- /solver/solver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "sort" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type board struct { 13 | blocks []int 14 | moves int 15 | count int 16 | difficulty int 17 | } 18 | 19 | var seed int 20 | var radius int 21 | var colors int 22 | var size int 23 | var minimum int = -1 24 | var tested int = 0 25 | var colorList []int 26 | var difficulty int 27 | var params parameters 28 | var failed bool 29 | 30 | func createBoard() *board { 31 | var moon board 32 | moon.blocks = make([]int, size*size) 33 | return &moon 34 | } 35 | 36 | // func randomFloat() float64 { 37 | // n := math.Sin(float64(seed)) * 10000 38 | // seed++ 39 | // return n - math.Floor(n) 40 | // } 41 | 42 | // func randomInt(n int) int { 43 | // var negative int 44 | // if n < 0 { 45 | // negative = -1 46 | // } else { 47 | // negative = 1 48 | // } 49 | // n *= negative 50 | // result := math.Floor(randomFloat() * float64(n)) 51 | // return int(result) * negative 52 | // } 53 | 54 | func makeColors(n int) []int { 55 | colorList = make([]int, n) 56 | for i := 0; i < n; i++ { 57 | colorList[i] = rand.Intn(0xffffff) 58 | } 59 | return colorList 60 | } 61 | 62 | func moonData() *board { 63 | moon := createBoard() 64 | makeColors(colors) 65 | radiusSquared := radius * radius 66 | for y := 0; y < size; y++ { 67 | for x := 0; x < size; x++ { 68 | dx := x - radius 69 | dy := y - radius 70 | distanceSquared := dx*dx + dy*dy 71 | if distanceSquared <= radiusSquared { 72 | color := rand.Intn(colors) 73 | moon.blocks[x+y*size] = color 74 | moon.count++ 75 | } else { 76 | moon.blocks[x+y*size] = -1 77 | } 78 | } 79 | } 80 | return moon 81 | 82 | } 83 | 84 | func draw(data *board) { 85 | var line string 86 | for y := 0; y < size; y++ { 87 | for x := 0; x < size; x++ { 88 | color := data.blocks[x+y*size] 89 | if color == -1 { 90 | line += " " 91 | } else { 92 | line += strconv.Itoa(color) 93 | } 94 | } 95 | fmt.Println(line) 96 | line = "" 97 | } 98 | } 99 | 100 | type solved struct { 101 | seed int 102 | minimum int 103 | moon *board 104 | colors []int 105 | difficulty int 106 | } 107 | 108 | func seedExists(s shootJSON) bool { 109 | for _, shoot := range s { 110 | if shoot.Seed == seed { 111 | return true 112 | } 113 | } 114 | return false 115 | } 116 | 117 | func simpleSeed() { 118 | nano := time.Now().UnixNano() 119 | seedString := fmt.Sprintf("%d", nano) 120 | seed, _ = strconv.Atoi(seedString[len(seedString)-6:]) 121 | rand.Seed(int64(seed)) 122 | } 123 | 124 | func solver(p parameters, shoot shootJSON) *solved { 125 | params = p 126 | for { 127 | nano := time.Now().UnixNano() 128 | seedString := fmt.Sprintf("%d", nano) 129 | seed, _ = strconv.Atoi(seedString[len(seedString)-6:]) 130 | if !seedExists(shoot) { 131 | break 132 | } 133 | } 134 | rand.Seed(int64(seed)) 135 | minimum = 1000 136 | difficulty = 0 137 | radius = p.Radius 138 | size = radius*2 + 1 139 | colors = p.Colors 140 | data := moonData() 141 | iterate(data) 142 | if minimum != 1000 { 143 | s := solved{seed: seed, minimum: minimum, moon: data, colors: colorList, difficulty: difficulty} 144 | return &s 145 | } 146 | return nil 147 | } 148 | 149 | func cloneData(data *board) *board { 150 | clone := createBoard() 151 | for i := 0; i < len(data.blocks); i++ { 152 | clone.blocks[i] = data.blocks[i] 153 | } 154 | clone.count = data.count 155 | clone.moves = data.moves + 1 156 | clone.difficulty = data.difficulty 157 | tested++ 158 | return clone 159 | } 160 | 161 | func inNeighbors(list []neighbor, x, y int) bool { 162 | for _, n := range list { 163 | if n.x == x && n.y == y { 164 | return true 165 | } 166 | } 167 | return false 168 | } 169 | 170 | func dir(data *board, x, y, deltaX, deltaY, color int, list, search []neighbor) ([]neighbor, []neighbor) { 171 | if x+deltaX >= 0 && x+deltaX < size && y+deltaY >= 0 && y+deltaY < size { 172 | check := data.blocks[x+deltaX+(y+deltaY)*size] 173 | if check != -1 && check == color && !inNeighbors(list, x+deltaX, y+deltaY) { 174 | list = append(list, neighbor{x + deltaX, y + deltaY}) 175 | search = append(search, neighbor{x + deltaX, y + deltaY}) 176 | } 177 | } 178 | return list, search 179 | } 180 | 181 | type neighbor struct { 182 | x int 183 | y int 184 | } 185 | 186 | func findNeighbors(data *board, x0, y0, color int) []neighbor { 187 | list := make([]neighbor, 0, 10) 188 | search := make([]neighbor, 0, 10) 189 | list = append(list, neighbor{x0, y0}) 190 | search = append(search, neighbor{x0, y0}) 191 | for len(search) != 0 { 192 | entry := search[0] 193 | search = search[1:] 194 | list, search = dir(data, entry.x, entry.y, 1, 0, color, list, search) 195 | list, search = dir(data, entry.x, entry.y, 0, 1, color, list, search) 196 | list, search = dir(data, entry.x, entry.y, 1, 1, color, list, search) 197 | list, search = dir(data, entry.x, entry.y, -1, 0, color, list, search) 198 | list, search = dir(data, entry.x, entry.y, 0, -1, color, list, search) 199 | list, search = dir(data, entry.x, entry.y, -1, -1, color, list, search) 200 | list, search = dir(data, entry.x, entry.y, 1, -1, color, list, search) 201 | list, search = dir(data, entry.x, entry.y, -1, 1, color, list, search) 202 | } 203 | return list 204 | } 205 | 206 | func inMoves(moves [][]neighbor, x, y int) bool { 207 | for _, move := range moves { 208 | for _, entry := range move { 209 | if entry.x == x && entry.y == y { 210 | return true 211 | } 212 | } 213 | } 214 | return false 215 | } 216 | 217 | type byNeighbors [][]neighbor 218 | 219 | func (s byNeighbors) Len() int { 220 | return len(s) 221 | } 222 | 223 | func (s byNeighbors) Swap(i, j int) { 224 | s[i], s[j] = s[j], s[i] 225 | } 226 | 227 | func (s byNeighbors) Less(i, j int) bool { 228 | return len(s[i]) > len(s[j]) 229 | } 230 | 231 | func gather(data *board) [][]neighbor { 232 | moves := make([][]neighbor, 0, 10) 233 | for y := 0; y < size; y++ { 234 | for x := 0; x < size; x++ { 235 | color := data.blocks[x+y*size] 236 | if color != -1 && !inMoves(moves, x, y) { 237 | moves = append(moves, findNeighbors(data, x, y, color)) 238 | } 239 | } 240 | } 241 | sort.Sort(byNeighbors(moves)) 242 | return moves 243 | } 244 | 245 | type movingType struct { 246 | x int 247 | y int 248 | xTo int 249 | yTo int 250 | } 251 | 252 | func inMovesTo(moving []movingType, xTo, yTo int) bool { 253 | for _, move := range moving { 254 | if move.xTo == xTo && move.yTo == yTo { 255 | return true 256 | } 257 | } 258 | return false 259 | } 260 | 261 | func compress(data *board) { 262 | next := true 263 | for next { 264 | moving := make([]movingType, 0, 10) 265 | for y := 0; y < size; y++ { 266 | for x := 0; x < size; x++ { 267 | color := data.blocks[x+y*size] 268 | if color != -1 && !(math.Abs(float64(x-radius)) < 1 && math.Abs(float64(y-radius)) < 1) { 269 | angle := math.Atan2(float64(radius-y), float64(radius-x)) 270 | xTo := int(math.Round(float64(x) + math.Cos(angle))) 271 | yTo := int(math.Round(float64(y) + math.Sin(angle))) 272 | if data.blocks[int(xTo)+int(yTo)*size] == -1 && !inMovesTo(moving, xTo, yTo) { 273 | moving = append(moving, movingType{x, y, xTo, yTo}) 274 | } 275 | } 276 | } 277 | } 278 | if len(moving) != 0 { 279 | for len(moving) != 0 { 280 | var move movingType 281 | move, moving = moving[len(moving)-1], moving[:len(moving)-1] 282 | data.blocks[move.xTo+move.yTo*size] = data.blocks[move.x+move.y*size] 283 | data.blocks[move.x+move.y*size] = -1 284 | } 285 | next = true 286 | } else { 287 | next = false 288 | } 289 | } 290 | } 291 | 292 | func clear(data *board, move []neighbor) { 293 | for _, neighbor := range move { 294 | data.blocks[neighbor.x+neighbor.y*size] = -1 295 | } 296 | data.count -= len(move) 297 | } 298 | 299 | func countColors(data *board, moves [][]neighbor) int { 300 | colors := make(map[int]bool) 301 | for _, move := range moves { 302 | colors[data.blocks[move[0].x+move[0].y*size]] = true 303 | } 304 | return len(colors) 305 | } 306 | 307 | func iterate(data *board) { 308 | moves := gather(data) 309 | if countColors(data, moves)+data.moves < minimum { 310 | for moveIndex, move := range moves { 311 | if failed { 312 | break 313 | } 314 | clone := cloneData(data) 315 | clone.difficulty += moveIndex 316 | clear(clone, move) 317 | compress(clone) 318 | if clone.count == 0 { 319 | if clone.moves <= minimum || (clone.moves == minimum && clone.difficulty > difficulty) { 320 | minimum = clone.moves 321 | difficulty = clone.difficulty 322 | // fmt.Println("New minimum: ", minimum) 323 | // fmt.Printf("\rMoves: %d, minimum %d", tested, minimum) 324 | } 325 | } else { 326 | if clone.moves < minimum { 327 | // fmt.Printf("\rMoves: %d, minimum %d", tested, minimum) 328 | iterate(clone) 329 | } 330 | } 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /script/script.js: -------------------------------------------------------------------------------- 1 | export const tutorial = [ 2 | 'Ah, the new employee. Glad to have you aboard!##We just reached a galaxy full of shiny moons. Moons that must be destroyed!', 3 | 'We’ll start you on a simple moon. We don’t want you scared away on your first day, eh?##The Moonerator has limited shots so choose your shots wisely.', 4 | ] 5 | 6 | export const story = [ 7 | 'Ah, the smell of your first moon destruction. Congrats!##Don’t worry, there’s more where that came from. We’ll warp you to the next moon when you’re ready.', 8 | 'I used to be in your chair. Well, not that exact chair, of course. But I was tasked with destruction sequence finding.##There’s something satisfying about the click of a perfect sequence.', 9 | 'If you grow tired of my babbling, you can disable it.##Although it is nice to talk to someone again. It grows lonely out here amongst the stars.', 10 | 'You’re probably wondering about the destruction sequence.##The colors indicate geological networks in the moon’s interior. The Moonerator Mark v.2.9 works best with limited shots.', 11 | 'My story? Are we friends now? Weird.##Well, I am the designer of the Moonerator. She runS smoothly, doesn’t she? Wasn’t always that way.', 12 | 'No, I wasn’t hired like you. That’s just silly. Professor Destruction, they called me, the destroyer of orbs!##Okay, nobody actually called me that. But they could have.', 13 | 'It’s not like I hate rocks. And I do ensure that there’s nothing living on these moons.##I’m not a monster. At least not anymore.', 14 | 'We have always been in the business of destruction. The Moonerator is the culmination of that journey.##It’s my greatest legacy.', 15 | 'Who funds us? You’re a curious one, aren’t you?##Once we discovered warp engines, you would think that money would stop being a thing.', 16 | 'We had abundant resources and energy that could be used for any purpose.##It’s shocking, I know, but even with free energy and uncountable resources, money was still a thing.', 17 | 'It turns out that it wasn’t the money that people craved, but power. And if everyone had infinite money, then nobody would have power.##Certain dreams turn out to be just that: dreams.', 18 | 'People still use money and therefore they need jobs.##I don’t need to tell you this since you’re here clicking away, earning those credits, right?', 19 | 'My purpose here? Well it’s our purpose really. It’s the destruction of moons.##It’s simple, pure, and it’s a goal you can get behind.', 20 | 'There’s not always an answer to the why question, so you can stop pestering me.##Just know that you’re a small cog in a large machine with a pure purpose. That’s got to provide job satisfaction, right?', 21 | 'I didn’t always destroy moons, of course. In the beginning, well, we started with a different aim. We were making things better.##Isn’t that how it always starts?', 22 | 'This was before you were born during the planet wars.##Warp had just been invented and while there were unlimited planets and systems to explore, some were more important than others.', 23 | 'It was the crossroad systems that mattered.##When you control where warps exit then you profit from any trade that passes through.', 24 | 'I was drafted into the war efforts to control those points.##I had a certain knack for destruction. I have other abilities, of course. But those in power were not interested in those abilities.', 25 | 'The wars were brutal. Destruction was constant.##The destruction sequence to destroy planets is the same as for moons. People and their cities have no impact on the sequences.', 26 | 'As quickly as humans expanded across galaxies, wars were only a step behind.##In those years, I spent all my time warping between systems and promoting the cause of . . . peace.', 27 | 'Peace is a funny word. The powerful seek it once they have power, but the weak avoid it until they have taken that power. And afterwards they too seek . . . peace.', 28 | 'You’re really getting a hang of these sequences. I had my doubts when you first started.##I’ll talk to my boss to see what we can do about your holiday bonus.', 29 | 'There were pockets of peace, of course. When powerful empires rose and controlled vast stretches of galaxies.##But they never lasted. Space is too big to govern effectively, and people are too greedy.', 30 | 'Don’t they teach you anything in those space schools?##Warping is what made these wonders possible. It was the bridge to the stars for humanity.', 31 | 'Warping is like a series of well-planned highways.##They’re fast but you still need to pass between adjacent exits to reach distant parts.', 32 | 'And it’s those adjacent exits where there can be trouble.##When there’s trouble in one exit then the following exits become unreachable. And there’s power in trouble.', 33 | 'Troubles created wars and wars created troubles. It’s a cycle.##I think it’s something innate in humans. They start the troubles so that they have something to fix.', 34 | 'Yeah, you would think that weapons would grow more destructive during the wars.##But like most things, there’s a limit.', 35 | 'I mean, sure, I could destroy entire stars but what’s the point?##And black holes would be fun to destroy but physics just won’t let us.', 36 | 'Now, black holes, those are energy generators. Harvesting energy from them was key to the early empires.', 37 | 'You haven’t heard about the black-hole empires? How quickly history is forgotten.##I can’t blame them. Human history is long and sad.', 38 | 'That not repeating history thing? Yeah, that’s only in storybooks.##History has a shape and it’s a repetitive shape. Given a long enough sequence, it will repeat in terrible ways.', 39 | 'Pessimist? Me? I brought you onboard, so that must show I have hope, right?##I can’t change the shape of history, but I can make parts of it more tolerable.', 40 | 'Wow, that was a particularly challenging sequence. Bravo!##Destroying moons sends a message to the system inhabitants. It’s known in the military arts as the projection of power.', 41 | 'There are vast swaths of humanity that are cut off from the rest of us. I think it’s for the best.##But we occasionally run into them when building new warp ways.', 42 | 'They’re not always friendly, of course. And we sometimes run across entire civilizations that didn’t make it.##If we arrive early enough then we can find their remains.', 43 | 'There was a time I thought things would be different. It was early in my planet-busting career.##Her name was General Xin and I ran across her in Sector 781A2.', 44 | 'General Xin made me think that there could be something there, you know?##A multigalaxy-wide coalition trying to make things better for everyone.', 45 | 'It was her charm that threw me off. She shone brighter than entire systems, and when she brought that to bear on you . . . even I was susceptible to it.', 46 | 'At first peace and prosperity seemed inevitable. We stampeded through systems, conquering, and establishing fair governments.', 47 | 'Our campaign was met with little effective resistance, and the people seemed happy to have a strong, charismatic leader.', 48 | 'But the more systems she conquered, the more power she craved, and the more ruthless she became.##She squeezed her systems to fund the conquering of new systems.', 49 | 'Eventually she paid the ultimate price for the squeezing. Oranges only have so much liquid before they’re only pulp.', 50 | 'After she lost control, I wandered the galaxies for a while. I was lost in the fall of her empire and nobody was looking for me.', 51 | 'The chaos closed most traffic on the warp ways. Except for me, of course.##I had an early version of the Moonerator, and you see what it does if there’s trouble.', 52 | 'I would project power but did not use it to conquer. Only to explore.##And it was through that exploration that I came up with my plan.', 53 | 'You’re a part of that plan, you know. You may think your job is only a way to put bread on your family’s table, but to me it’s much more.', 54 | 'You’re the face of the next generation of peace. Peace always has a cost. I tried to mitigate the cost and the cheapest price I could find was the moons.', 55 | 'You and the other destruction sequence technicians project our power and keep the systems in line.##There is no person or government in charge anymore.', 56 | 'There are only the seemingly random movements of the fleet of Moonerators.##Cause trouble and moons explode in your system.', 57 | 'Don’t cause trouble, and the moons still occasionally explode.##Power must be exercised, or it loses its, well, its power.', 58 | 'What results is not the utopia that General Xin dreamed about.##Instead, it’s a steady governance where power and fear are meted out in proportions.', 59 | 'There are well-governed systems, and disastrous ones. Our fleet does not interfere as much as I would like it to.##Our role is to maintain the peace between systems.', 60 | 'I have been around for hundreds of millennia. And this was the most perfect system I can come up with.##It’s a terrible system and people still suffer.', 61 | 'But like democracy, as terrible as this is, the alternative is much worse.##Now, enough jabbering. Keep pounding out those destruction sequences.##Your talkative friend and employer,# A.I. 34C1-98A.', 62 | ] 63 | 64 | export const endScreen = [ 65 | 'Thank you for playing Shoot the Moon (like literally) developed for Github Game Off 2020!##To see the source code and check out my other games, please visit my website at: https://yopeyopey.com/##-David Figatner', 66 | ] -------------------------------------------------------------------------------- /images/ui.editor.json: -------------------------------------------------------------------------------- 1 | {"zoom":4,"current":0,"imageData":[{"undo":[{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAQklEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBViEyTKMzAPEfQ1tuAChwIuCXRx6isEADihLAwLCAPvAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAARklEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBViEyTKMzAPEfQ1SQqxhS1GOGJTBA5XXBLo4gBurzAMPIbDPAAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAQklEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBViEyTKMzAPEfQ1bRRiiwSMAMemCCQGAAC9NAzzd9DYAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAO0lEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBViEyTKMzAPEfT1ACvEFlsA7q04DIEovmMAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAOUlEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBViEyTKMzAPEfT1IFQIADisPAxFF4JuAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAQElEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBViEyTKMzAPEfQ19RUSFY6wmMDmGQBInDwMpnDTwQAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAARElEQVQoU42QwQoAMAhC5/9/tKNDUOGaXaJ8RIojiiQBoEptCCGg6CuYl9aLVXyCU5CgWlpm0tDXtQVarjNcK8cKzx8v+ptADPqxUVMAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAR0lEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBViEyTKMzAP4fQMUW6EhRdRvkZWTDAc0U1GjhkAYLhEDKTI6psAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAQElEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBViE6TMRFgQEOVGdMXUC0dszoCJAQB61UgMtUR0AgAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAQElEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcJMwmsisiROhegSWBVicxNlJsKCgCg3oiumXjhicwZMDAB6xUgM4Se/vAAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAQElEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcIU4VWIrAinQnRFWBViU0SZibAgIMqN2BRTJxyRTUY3EQAGbSwMzMwQ5gAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcIU4VWIrAhmJbKpYKsJKQLbgE0RTquJMhHmHnTFlPsa3WSCAQ4LBXSFAAZdLAxkwF8pAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcIU4VWIrAjFSqgTwFZjU4RhNSGTYKYTbyJMB7rJlPsa3WSCAQ4LBXSFAAZNLAzptSXYAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASUlEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcIU4VWIrAjdNSDTwVZjU4RhNS6TsLqRKBNh7kFXTLmv0U0mGOCwUEBXCAAGPSwM4vC7KAAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAR0lEQVQoU2NkwAL+////n5GRkRFZCoUDkgApAtF4FcIU4VWIrAibc8BWY1OEYTUuk7C6kSgTYe5BV0y5r9FNJhjgsFBAVwgAAbcsCLt/YTYAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASklEQVQoU6WQQQoAMAjD1v8/ukNBUOn0MC9DDDUTRxRJAkAelcYGBtk7ggGNYIaUjq9WUF7tzq8k6bglunP4dPj/1z15PXhcoYMX4AIwBBPRVasAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASklEQVQoU6WQ0QoAIAgD2/9/9EJhYGYW1IuExzzFKB5JAkBsLR9rGGS1BQW1YIQqHR9dQXG0O5+SNvAl0Z3lk5P/t87J14PLOYMTGwA0AFnebUIAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU6WQUQoAIAhD2/0PvVAYmJkF+SOyx5xiFEWSABClZTDBIOstKKgFI1TF8dUVFFd75pPTBj47Kk92/r86O18frswZnLKEN/wBCMMFAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU62Q0QoAIAgD2/9/9EIhWLakh3wR8TiHGKZIEgB0tQ2xCCh6Cy6oBRVycfK0g/R0Zr6ZDvDZuPJUszU6+M8f1VyNE6asO/h86FXYAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU42Q0QoAIAgD2/9/9EJBWLYkX0Q8ziGWKZIEAF0dQywCij6CBY2gQi5OnnaQns7ML9MFfhsrTzdbo4NH8PuPau4P3/dpP/RN2oi+AAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATElEQVQoU42Q0QoAIAgD2/9/9ELBMFtiLxEe5xqWOCQJAHl0PWxgkN0tGFALZkjF8dUKyqs988/0gGNj5KlmaVRwgKeu+sNRj9lcC98HaD/0IQ4mqgAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATElEQVQoU42Q0QoAIAgD2/9/tKEgTFtiLxEe5xqOOGZmAMCj8vCBQ36PYEIjyJCKE6sVxKsj88/0gGtj5ulmaVSwg6WF/sNVj2zuhV8XWD/0z2pa+AAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU6WQ0QoAIAgD2/9/9EJBMJsS5IuIxznEEkWSAJBXx2ALg6yPYEAjmCEVx08rKJ/2zJ3pAp+NkaeapbGD//+YzfXhG6UqO/Q/4l4UAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASklEQVQoU52Q0QoAIAgD2/9/9EJBWGUj8iXEw11iNEWSAKCjpYlBQPFasCALKtTpZHQHaXQ63zYd4PPG8nHxGa3i7kN/d1SN/eATpRo79L3dsbsAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATklEQVQoU42Q0QoAIAgD2/9/tOFAmKZSLxEetyVOc8zMAEBH6eEDh/xewYBWUKGuDqM7SKPZeTI94GTUnjRGny0+gZuZ9vrDrz1qjbrwC6UKO/TMDgKPAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATklEQVQoU42RUQoAIAhD2/0PvVAwVtiqHwlf2zSM5pAkAGhru0QjoKgWLMiCCnVx0rqD1DozO6WCE7wpas4Fvuw30Cmn+jnh1x7rUfczE6T6O/TAlYvwAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASUlEQVQoU52Q0QoAIAgD2/9/9MJgoGUj6kVixznEaB5JAkCOyieCgGJaUJAFM9TVWatvkNavzs5UwGej+nTmw+hgZX93zOb94BOk6jv0lW5KhAAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATElEQVQoU42Q0QoAIAgD2/9/9GKCYCKzXiQ85inO8EgSAGrr+aghSNWCCVmwQpNOjHaQFMJ5gzLIJuZCkZg+U/IIrq59w687Vo1+8Auk2jv0nhZfOQAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU53RUQoAIAgD0Hb/Qy8UBKM1oX4ifOQyLLFIEgB66ThEIVDsFhaysCMVJ1tPKDs4VDnzce7GC76whFPWvznWWNTPbMKON/SgCIpaAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU53RUQoAIAgD0Hb/Qy8UBKM1oX4ifNQ0LLFIEgB66ThEIVDsFhaysCMVJ58eUeRwqHJmc+7GC76whFPWvznWWNTPbMZwN/g3jpfmAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASUlEQVQoU52Q0QoAIAgD2/9/9GKBsGIo5IuIhx7DCkWSAOCra9BCkHoLFtSCDiWd83qE5NFB7tleLPCkUD7pcgQn178cXeMNfAOLgTP8mbsKrQAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAAS0lEQVQoU42R0QoAIAgD2/9/9GKCYGWrXiQ88zSM5pAkANTUclFCkKIFE7JghTqdaP2E5OGg6mlfPMBb+wRjXVXcauwTfu0xi7qfmfPuMADDjDbjAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATUlEQVQoU42Q0QoAIAgD2/9/tLHAWCarXiI81k2M5kREAICOjgcHhHhbMCELKtTprK+fED0clJ6rnEvUQrt1l3wlprzVqA2/9qjJdeETLuwz/HND9uoAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATUlEQVQoU42QUQoAIAhD2/0PvVAwVsnKHxEfcxOjKZIEAF1tQywCim7BgiyoUGcnT7+gvOCg8pnhnKIGWqk75UuxzFsbZ8KvP6ry+fAJKwoz+C0QyZcAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU53RUQoAIAgD0Hb/Qy8UBKM1oX4ifOQyLLFIEgB66ThEIVDsFhaysCMVJ1tPKDs4VDnzce7GC76whFPWvznWWNTPbMKON/SgCIpaAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATElEQVQoU52QUQoAIAhD2/0PbUxYWMmC/BHZY04xmoqIAIAqbQMFQuwWFGTBCnVxcvULyg0OUs48zjkSXMcpT+d8OTpY2t8fq/P58AnSfjf0H7t14AAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATklEQVQoU42RQQ4AIQgD7f8fXVMSDCrb1QsxDDIgRnNIEgBqarsoIUjRgglZsEKdTrT+g6KDg+S5nN2LF/gF5+SxripuNc4Jn/aYRd3PTOJuN/TAtDm2AAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAATUlEQVQoU52QUQoAIAhD2/0PvdjAsBCL/BHxodswiiJJAMirbdBCkHoLBtSCGark+PUN8ocXyGB3UYaWudBTXQ7njisL72T85ZhlnIFPlDEz9KLRJYsAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASklEQVQoU52Q0QoAIAgD2/9/9MKBsEJM8iXEQ69hFUWSAOCjo4lBQPG2YEIt6FClo9MvSBcmkMDxxvSpNufPFZeLdxp/ObrGHfgG6fQv9GiHPgIAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASklEQVQoU52Q0QoAIAgD2/9/9GIDwcIs8iXEQ69hFEWSAJBHS6OBIL0tGFALZqjS8ekb5AsvkMHnjeFz2qzfO64s3mn85Zg19sAn6eQv9K/nbLkAAAAASUVORK5CYII="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAARklEQVQoU52QUQoAMAhC5/0P7SgIbETF+onwYSJOMSQJACqlwwSDbLdgQC2oUBXHX0+Qf9hADq4dI8/knOrp4L8eNcZb+AXztiv0tB84MwAAAABJRU5ErkJggg=="},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU52QUQoAMAhC5/0P7TAIarQW6yfCh4lYxZAkAEQpHRIEabegQy0YoSqOvX5B9mECGTh29Dwv51TPDVZVfz3GGKfJBvUIK/gWD4tIAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU52QUQoAMAhC5/0P7SgIbETF+onwYSJOMSQJACqlwwSDbLdgQC2oUBXHX0+Qf9hADq4dI8/knOqp4Kjpr0eN8RZ+AfUYK/gRnXolAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAASElEQVQoU52QUQoAMAhC5/0P7SgYWETF+onwYSJOMSQJACqFwwSDbLfgg1pQoSqOv54g/7CBHFw7vjyTc6gnw1rRX48aIxd+AfUoK/iajEHLAAAAAElFTkSuQmCC"},{"width":10,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAYAAABGbhwYAAAARklEQVQoU42Q0QoAUARD7/7/o3ezUkjwIu00A68pkgSAKKXBBIOsj6BDIxihLo5Wb5A2XCCBZ0fPszmn95yurs7rwz1zBT/1OCv4Kuw/mgAAAABJRU5ErkJggg=="},{"width":11,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAASElEQVQoU5XQUQoAIAgD0Hb/QxsGE6nBrM96tiGWOBERAHA/PRcJE1lMaHGHqlomnRoOMgkTWPjrZ/ZzCdVZDdjV9UojzAGFNyadLAwQ62VGAAAAAElFTkSuQmCC"},{"width":11,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAASElEQVQoU5XQUQoAIAgD0Hb/Qy8MFDNh1mc9mwyrOSQJAPXpuTBoSGKHEmfYrWZJZw0FPQkTGPjrZ99vknBVN26jJsie80CHNyR3LAg9bNlAAAAAAElFTkSuQmCC"},{"width":11,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAASUlEQVQoU43QUQoAIAgD0Hb/Qy8MDDNh67OeTYY1HJIEgP70XQQMJHFCiSucVouks4aCmQQHXuz+fAZyPyfhqc5uoyfInuvAhDchnSwEV9vVwwAAAABJRU5ErkJggg=="},{"width":11,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAASElEQVQoU42QQQoAMAjD1v8/ukNBcE5oPWo0UpylSBIA5uhrBBiQhAuUcAe318KUbyiwTHDAMtmX83ptOYYnOjuNaZA594UNvh4PLABf7balAAAAAElFTkSuQmCC"},{"width":11,"height":11,"data":"iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAR0lEQVQoU5WQUQoAMAhC5/0P7SgIqgW2PuuZIs4wJAkA/fQsDDRIwgFKOINTNHPyGAoMMbagR/v6HBYbh1Lduo3uIHvOggm+Gc0r/LlQQpsAAAAASUVORK5CYII="}],"redo":[]}],"viewport":{"x":645.3439315148719,"y":207.54302446362038,"scale":0.727111270261023}} 2 | -------------------------------------------------------------------------------- /dist/www.9ad09f98.js: -------------------------------------------------------------------------------- 1 | // modules are defined as an array 2 | // [ module function, map of requires ] 3 | // 4 | // map of requires is short require name -> numeric require 5 | // 6 | // anything defined in a previous bundle is accessed via the 7 | // orig method which is the require for previous bundles 8 | parcelRequire = (function (modules, cache, entry, globalName) { 9 | // Save the require from previous bundle to this closure if any 10 | var previousRequire = typeof parcelRequire === 'function' && parcelRequire; 11 | var nodeRequire = typeof require === 'function' && require; 12 | 13 | function newRequire(name, jumped) { 14 | if (!cache[name]) { 15 | if (!modules[name]) { 16 | // if we cannot find the module within our internal map or 17 | // cache jump to the current global require ie. the last bundle 18 | // that was added to the page. 19 | var currentRequire = typeof parcelRequire === 'function' && parcelRequire; 20 | if (!jumped && currentRequire) { 21 | return currentRequire(name, true); 22 | } 23 | 24 | // If there are other bundles on this page the require from the 25 | // previous one is saved to 'previousRequire'. Repeat this as 26 | // many times as there are bundles until the module is found or 27 | // we exhaust the require chain. 28 | if (previousRequire) { 29 | return previousRequire(name, true); 30 | } 31 | 32 | // Try the node require function if it exists. 33 | if (nodeRequire && typeof name === 'string') { 34 | return nodeRequire(name); 35 | } 36 | 37 | var err = new Error('Cannot find module \'' + name + '\''); 38 | err.code = 'MODULE_NOT_FOUND'; 39 | throw err; 40 | } 41 | 42 | localRequire.resolve = resolve; 43 | localRequire.cache = {}; 44 | 45 | var module = cache[name] = new newRequire.Module(name); 46 | 47 | modules[name][0].call(module.exports, localRequire, module, module.exports, this); 48 | } 49 | 50 | return cache[name].exports; 51 | 52 | function localRequire(x){ 53 | return newRequire(localRequire.resolve(x)); 54 | } 55 | 56 | function resolve(x){ 57 | return modules[name][1][x] || x; 58 | } 59 | } 60 | 61 | function Module(moduleName) { 62 | this.id = moduleName; 63 | this.bundle = newRequire; 64 | this.exports = {}; 65 | } 66 | 67 | newRequire.isParcelRequire = true; 68 | newRequire.Module = Module; 69 | newRequire.modules = modules; 70 | newRequire.cache = cache; 71 | newRequire.parent = previousRequire; 72 | newRequire.register = function (id, exports) { 73 | modules[id] = [function (require, module) { 74 | module.exports = exports; 75 | }, {}]; 76 | }; 77 | 78 | var error; 79 | for (var i = 0; i < entry.length; i++) { 80 | try { 81 | newRequire(entry[i]); 82 | } catch (e) { 83 | // Save first error but execute all entries 84 | if (!error) { 85 | error = e; 86 | } 87 | } 88 | } 89 | 90 | if (entry.length) { 91 | // Expose entry point to Node, AMD or browser globals 92 | // Based on https://github.com/ForbesLindesay/umd/blob/master/template.js 93 | var mainExports = newRequire(entry[entry.length - 1]); 94 | 95 | // CommonJS 96 | if (typeof exports === "object" && typeof module !== "undefined") { 97 | module.exports = mainExports; 98 | 99 | // RequireJS 100 | } else if (typeof define === "function" && define.amd) { 101 | define(function () { 102 | return mainExports; 103 | }); 104 | 105 | //