├── 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 | //