├── src ├── assets │ ├── a.mid │ ├── a.mp3 │ ├── b.mid │ ├── b.mp3 │ ├── c.mid │ ├── c.mp3 │ ├── d.mid │ ├── d.mp3 │ ├── bead.png │ ├── card.png │ ├── dot.png │ ├── land.cpt │ ├── land.wav │ ├── break.cpt │ ├── break.wav │ ├── button.png │ ├── cover.png │ ├── cover.psd │ ├── crisp.cpt │ ├── crisp.wav │ ├── hpbar.png │ ├── jump1.cpt │ ├── jump1.wav │ ├── jump2.cpt │ ├── jump2.wav │ ├── jump3.cpt │ ├── jump3.wav │ ├── night.cpt │ ├── night.wav │ ├── player.png │ ├── smoke.png │ ├── solid.png │ ├── crumble.png │ ├── explode.cpt │ ├── explode.wav │ ├── gameplay.gif │ ├── spinner.png │ ├── drawbridge.cpt │ ├── drawbridge.png │ ├── drawbridge.wav │ ├── translucent.png │ ├── drawbridgeAlt.png │ ├── index.js │ └── maps.txt ├── scaffolding │ ├── twitchLogo.png │ ├── twitterLogo.png │ ├── index.jsx │ ├── Embed.jsx │ ├── Spinner.jsx │ ├── App.jsx │ ├── lib │ │ ├── sprites.js │ │ ├── anims.js │ │ ├── vector.js │ │ ├── camera.js │ │ ├── store.js │ │ ├── proxy.js │ │ ├── transitions.js │ │ ├── assets.js │ │ ├── tweens.js │ │ ├── level-parser.js │ │ ├── particles.js │ │ ├── shaders.js │ │ └── props.js │ ├── Production.css │ ├── boot-scene.js │ ├── Development.css │ ├── Logging.css │ ├── Controls.jsx │ ├── Development.jsx │ ├── DoubleEnder.css │ ├── Engine.css │ ├── Manage.jsx │ ├── base.css │ ├── Replay.css │ ├── Production.jsx │ ├── Logging.jsx │ ├── Manage.css │ ├── Controls.css │ ├── DoubleEnder.jsx │ └── Engine.jsx ├── game.js └── props.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── config ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── pnpTs.js ├── modules.js ├── paths.js ├── env.js └── webpackDevServer.config.js ├── .gitignore ├── scripts ├── retype-asset.pl ├── expand-asset.pl ├── remove-asset.pl ├── blog.sh ├── make-mp3s.pl ├── test.js ├── watch-sync.pl ├── add-asset.pl ├── Assets.pm ├── start.js ├── ld-timer.pl ├── build.js └── ld-timer-output.pl ├── LICENSE.md ├── README.md └── package.json /src/assets/a.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/a.mid -------------------------------------------------------------------------------- /src/assets/a.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/a.mp3 -------------------------------------------------------------------------------- /src/assets/b.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/b.mid -------------------------------------------------------------------------------- /src/assets/b.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/b.mp3 -------------------------------------------------------------------------------- /src/assets/c.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/c.mid -------------------------------------------------------------------------------- /src/assets/c.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/c.mp3 -------------------------------------------------------------------------------- /src/assets/d.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/d.mid -------------------------------------------------------------------------------- /src/assets/d.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/d.mp3 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/bead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/bead.png -------------------------------------------------------------------------------- /src/assets/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/card.png -------------------------------------------------------------------------------- /src/assets/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/dot.png -------------------------------------------------------------------------------- /src/assets/land.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/land.cpt -------------------------------------------------------------------------------- /src/assets/land.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/land.wav -------------------------------------------------------------------------------- /src/assets/break.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/break.cpt -------------------------------------------------------------------------------- /src/assets/break.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/break.wav -------------------------------------------------------------------------------- /src/assets/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/button.png -------------------------------------------------------------------------------- /src/assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/cover.png -------------------------------------------------------------------------------- /src/assets/cover.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/cover.psd -------------------------------------------------------------------------------- /src/assets/crisp.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/crisp.cpt -------------------------------------------------------------------------------- /src/assets/crisp.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/crisp.wav -------------------------------------------------------------------------------- /src/assets/hpbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/hpbar.png -------------------------------------------------------------------------------- /src/assets/jump1.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/jump1.cpt -------------------------------------------------------------------------------- /src/assets/jump1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/jump1.wav -------------------------------------------------------------------------------- /src/assets/jump2.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/jump2.cpt -------------------------------------------------------------------------------- /src/assets/jump2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/jump2.wav -------------------------------------------------------------------------------- /src/assets/jump3.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/jump3.cpt -------------------------------------------------------------------------------- /src/assets/jump3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/jump3.wav -------------------------------------------------------------------------------- /src/assets/night.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/night.cpt -------------------------------------------------------------------------------- /src/assets/night.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/night.wav -------------------------------------------------------------------------------- /src/assets/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/player.png -------------------------------------------------------------------------------- /src/assets/smoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/smoke.png -------------------------------------------------------------------------------- /src/assets/solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/solid.png -------------------------------------------------------------------------------- /src/assets/crumble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/crumble.png -------------------------------------------------------------------------------- /src/assets/explode.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/explode.cpt -------------------------------------------------------------------------------- /src/assets/explode.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/explode.wav -------------------------------------------------------------------------------- /src/assets/gameplay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/gameplay.gif -------------------------------------------------------------------------------- /src/assets/spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/spinner.png -------------------------------------------------------------------------------- /src/assets/drawbridge.cpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/drawbridge.cpt -------------------------------------------------------------------------------- /src/assets/drawbridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/drawbridge.png -------------------------------------------------------------------------------- /src/assets/drawbridge.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/drawbridge.wav -------------------------------------------------------------------------------- /src/assets/translucent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/translucent.png -------------------------------------------------------------------------------- /src/assets/drawbridgeAlt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/assets/drawbridgeAlt.png -------------------------------------------------------------------------------- /src/scaffolding/twitchLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/scaffolding/twitchLogo.png -------------------------------------------------------------------------------- /src/scaffolding/twitterLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartak/nighty-night-nosferatu/HEAD/src/scaffolding/twitterLogo.png -------------------------------------------------------------------------------- /src/scaffolding/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './base.css'; 4 | import App from './App'; 5 | 6 | const debug = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | -------------------------------------------------------------------------------- /src/scaffolding/Embed.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Production.css'; 3 | import Engine from './Engine'; 4 | 5 | export default class Embed extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | /.ld-timer-state 26 | /.ld-timer-log 27 | -------------------------------------------------------------------------------- /scripts/retype-asset.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use lib 'scripts'; 5 | use Assets; 6 | 7 | @ARGV == 2 or die "usage: $0 name newtype\n"; 8 | my ($name, $type) = @ARGV; 9 | 10 | my $assets_file = 'src/assets/index.js'; 11 | my $assets = parse_assets($assets_file); 12 | 13 | die "no asset named $name" if !$assets->{$name}; 14 | $type = canonicalize_asset_type($type); 15 | 16 | $assets->{$name}{type} = $type; 17 | 18 | emit_and_diff_assets($assets_file, $assets); 19 | -------------------------------------------------------------------------------- /scripts/expand-asset.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use lib 'scripts'; 5 | use Assets; 6 | 7 | @ARGV == 1 or die "usage: $0 name\n"; 8 | my ($name) = @ARGV; 9 | 10 | my $assets_file = 'src/assets/index.js'; 11 | my $assets = parse_assets($assets_file); 12 | 13 | die "no asset named $name" if !$assets->{$name}; 14 | die "$name already expanded" if $assets->{$name}{extra}; 15 | 16 | $assets->{$name}{extra} = [ 17 | " file: ${name},", 18 | ]; 19 | 20 | emit_and_diff_assets($assets_file, $assets); 21 | -------------------------------------------------------------------------------- /src/scaffolding/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Spinner extends React.Component { 4 | render() { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /scripts/remove-asset.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use lib 'scripts'; 5 | use Assets; 6 | use File::Spec; 7 | 8 | @ARGV == 1 or die "usage: $0 name\n"; 9 | my ($name) = @ARGV; 10 | 11 | my $assets_file = 'src/assets/index.js'; 12 | my $assets = parse_assets($assets_file); 13 | 14 | die "no asset named $name" if !$assets->{$name}; 15 | 16 | my $path = File::Spec->catfile('src', 'assets', $assets->{$name}{path}); 17 | 18 | delete $assets->{$name}; 19 | 20 | emit_and_diff_assets($assets_file, $assets); 21 | system("git", "rm", $path); 22 | -------------------------------------------------------------------------------- /src/scaffolding/App.jsx: -------------------------------------------------------------------------------- 1 | import {hot} from 'react-hot-loader/root'; 2 | import React from 'react'; 3 | import Development from './Development'; 4 | import Production from './Production'; 5 | import Embed from './Embed'; 6 | import {productionDisplay} from '../../package.json'; 7 | 8 | class App extends React.Component { 9 | render() { 10 | const {debug} = this.props; 11 | 12 | if (productionDisplay.embed) { 13 | return ; 14 | } else if (debug) { 15 | return ; 16 | } else { 17 | return ; 18 | } 19 | } 20 | } 21 | 22 | export default hot(App); 23 | -------------------------------------------------------------------------------- /src/scaffolding/lib/sprites.js: -------------------------------------------------------------------------------- 1 | let injected = false; 2 | export function injectAddSpriteTimeScale(scene) { 3 | if (injected) { 4 | return; 5 | } 6 | 7 | // eslint-disable-next-line no-proto 8 | const proto = scene.physics.add.__proto__; 9 | 10 | const origAdd = proto.sprite; 11 | 12 | proto.sprite = function(...args) { 13 | const sprite = origAdd.call(this, ...args); 14 | if (sprite && sprite.anims) { 15 | sprite.anims.setTimeScale(sprite.scene.timeScale); 16 | sprite.anims.ignoresScenePause = false; 17 | } 18 | return sprite; 19 | }; 20 | 21 | injected = true; 22 | } 23 | -------------------------------------------------------------------------------- /src/scaffolding/lib/anims.js: -------------------------------------------------------------------------------- 1 | let injected = false; 2 | export function injectAnimationUpdate(animation) { 3 | if (injected || !animation) { 4 | return injected; 5 | } 6 | 7 | // eslint-disable-next-line no-proto 8 | const proto = animation.__proto__; 9 | 10 | const origUpdate = proto.update; 11 | 12 | proto.update = function(...args) { 13 | if (this.parent.scene._paused.anims && !this.ignoresScenePause && (!this.currentAnim || !this.currentAnim.ignoresScenePause)) { 14 | return; 15 | } 16 | 17 | return origUpdate.call(this, ...args); 18 | }; 19 | 20 | injected = true; 21 | 22 | return injected; 23 | } 24 | -------------------------------------------------------------------------------- /scripts/blog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/fish 2 | set -x DIR "$argv" 3 | 4 | set -x PUBLIC_URL "https://sartak.org/$DIR/" 5 | 6 | npm run build 7 | 8 | cat build/index.html | perl -nle 'while ($_ =~ m{}g) { my $s = $1; next if $s =~ /function gtag/; print $s }' > build/static/wrapper.js 9 | 10 | rsync --delete -avz build/static/ giedi-prime:devel/sartak.org/static/$DIR/static 11 | 12 | ls -1 build/static/css | grep -v '.map$' | perl -ple '$_ = "\@styles: /$ENV{DIR}/static/css/$_"' 13 | 14 | echo "@scripts: /$DIR/static/wrapper.js" 15 | ls -1 build/static/js | grep -v '^runtime' | grep -v '.map$' | perl -ple '$_ = "\@scripts: /$ENV{DIR}/static/js/$_"' 16 | -------------------------------------------------------------------------------- /src/scaffolding/Production.css: -------------------------------------------------------------------------------- 1 | body.ld-slate .production .game-metadata img { 2 | height: 1.4em; 3 | width: 1.4em; 4 | vertical-align: bottom; 5 | } 6 | 7 | body.ld-slate .production .game-metadata .url { 8 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 9 | monospace; 10 | font-size: 0.8em; 11 | } 12 | 13 | body.ld-slate .production .game-metadata { 14 | margin-top: 2em; 15 | text-align: center; 16 | font-size: 1.2em; 17 | line-height: 1.6em; 18 | } 19 | 20 | body.ld-slate .production .game-metadata a { 21 | text-decoration: none; 22 | } 23 | 24 | body.ld-slate .production .game-metadata h2 { 25 | font-style: italic; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /config/pnpTs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolveModuleName } = require('ts-pnp'); 4 | 5 | exports.resolveModuleName = ( 6 | typescript, 7 | moduleName, 8 | containingFile, 9 | compilerOptions, 10 | resolutionHost 11 | ) => { 12 | return resolveModuleName( 13 | moduleName, 14 | containingFile, 15 | compilerOptions, 16 | resolutionHost, 17 | typescript.resolveModuleName 18 | ); 19 | }; 20 | 21 | exports.resolveTypeReferenceDirective = ( 22 | typescript, 23 | moduleName, 24 | containingFile, 25 | compilerOptions, 26 | resolutionHost 27 | ) => { 28 | return resolveModuleName( 29 | moduleName, 30 | containingFile, 31 | compilerOptions, 32 | resolutionHost, 33 | typescript.resolveTypeReferenceDirective 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/scaffolding/boot-scene.js: -------------------------------------------------------------------------------- 1 | import SuperScene from './SuperScene.js'; 2 | 3 | export default class BootScene extends SuperScene { 4 | static key() { 5 | return 'BootScene'; 6 | } 7 | 8 | constructor() { 9 | super({key: BootScene.key()}); 10 | } 11 | 12 | preload() { 13 | super.preload(); 14 | 15 | this.game.preloadScenes.forEach((sceneClass) => { 16 | if (sceneClass.name === this.name) { 17 | return; 18 | } 19 | 20 | const scene = new sceneClass(); 21 | scene.preload.call(this); 22 | }); 23 | } 24 | 25 | create() { 26 | super.create(); 27 | 28 | this.game.initializeShaders(); 29 | 30 | setTimeout(() => { 31 | this.game.preloadComplete(); 32 | }); 33 | } 34 | 35 | saveStateFieldName() { 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/scaffolding/lib/vector.js: -------------------------------------------------------------------------------- 1 | export const Distance = (dx, dy) => { 2 | return Math.sqrt(dx ** 2 + dy ** 2); 3 | }; 4 | 5 | export const NormalizeVector = (dx, dy) => { 6 | const d = Distance(dx, dy); 7 | return [dx / d, dy / d]; 8 | }; 9 | 10 | export const NormalizeVectorWithDistance = (dx, dy) => { 11 | const d = Distance(dx, dy); 12 | return [dx / d, dy / d, d]; 13 | }; 14 | 15 | export const SumVectors = (vs) => { 16 | let tx = 0; 17 | let ty = 0; 18 | 19 | vs.foreach((vx, vy) => { 20 | tx += vx; 21 | ty += vy; 22 | }); 23 | 24 | return [tx, ty]; 25 | }; 26 | 27 | export const CentroidPoints = (ps) => { 28 | let tx = 0; 29 | let ty = 0; 30 | 31 | ps.foreach((px, py) => { 32 | tx += px; 33 | ty += py; 34 | }); 35 | 36 | return [tx / ps.length, ty / ps.length]; 37 | }; 38 | -------------------------------------------------------------------------------- /scripts/make-mp3s.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | 5 | @ARGV == 1 or die "usage: ls *.mid | make-mp3.pl soundfonts/Setzers_SPC_Soundfont.sf2\n"; 6 | my $sf_file = shift; 7 | die "soundfont file '$sf_file' doesn't exist" unless -e $sf_file; 8 | 9 | (my $sf = $sf_file) =~ s/\.\w+$//; 10 | $sf =~ s!.*/!!; 11 | 12 | while (<>) { 13 | chomp; 14 | die "$_ doesn't exist" unless -e $_; 15 | my $mid = $_; 16 | my $wav = $_; 17 | $wav =~ s/\.mid$/-$sf.wav/; 18 | 19 | if (!-e $wav) { 20 | system( 21 | "fluidsynth", 22 | "-F", $wav, 23 | $sf_file, 24 | $mid, 25 | ); 26 | } 27 | 28 | my $mp3 = $_; 29 | $mp3 =~ s/\.mid$/-$sf.mp3/; 30 | 31 | if (!-e $mp3) { 32 | system( 33 | "ffmpeg", 34 | "-i", $wav, 35 | $mp3, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/scaffolding/Development.css: -------------------------------------------------------------------------------- 1 | body.ld-slate.natural .development #engine-container { 2 | margin-top: 16px; 3 | margin-left: 16px; 4 | position: absolute; 5 | } 6 | 7 | body.ld-slate .development .sidebar { 8 | margin-left: 840px; 9 | overflow: scroll; 10 | box-sizing: border-box; 11 | padding-bottom: 2em; 12 | height: 100vh; 13 | } 14 | 15 | body.ld-slate .development .sidebar > ul { 16 | list-style-type: none; 17 | padding: 0; 18 | text-align: center; 19 | } 20 | 21 | body.ld-slate .development .links { 22 | margin-bottom: 0; 23 | } 24 | 25 | body.ld-slate .development .links li { 26 | display: inline-block; 27 | margin-right: 0.5em; 28 | } 29 | 30 | body.ld-slate .development .links li a { 31 | text-decoration: none; 32 | color: blue; 33 | } 34 | 35 | body.ld-slate.scaled .development .sidebar, 36 | body.ld-slate.scaled .development .Logging { 37 | display: none; 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019-2022 Shawn M Moore 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | import SuperGame from "./scaffolding/SuperGame"; 2 | import proxyClass from "./scaffolding/lib/proxy"; 3 | 4 | import PlayScene from "./play-scene"; 5 | 6 | const baseConfig = { 7 | transparent: true, 8 | "render.transparent": true, 9 | }; 10 | 11 | export default class Game extends SuperGame { 12 | constructor(options) { 13 | const config = { 14 | ...baseConfig, 15 | ...options, 16 | }; 17 | super(config, [PlayScene]); 18 | } 19 | 20 | launch() { 21 | this.scene.add(`scene-${Date.now()}`, PlayScene, true, { 22 | seed: Date.now(), 23 | sceneId: String(Math.random()), 24 | }); 25 | } 26 | } 27 | 28 | if (module.hot) { 29 | { 30 | const proxy = proxyClass(PlayScene); 31 | module.hot.accept("./play-scene", () => { 32 | const Next = require("./play-scene").default; 33 | window.game.scene.scenes.forEach((scene) => { 34 | if (scene.constructor.name === Next.name) { 35 | proxyClass(Next, scene, proxy); 36 | } 37 | }); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Nighty Night, Nosferatu](https://github.com/sartak/nighty-night-nosferatu/blob/master/src/assets/cover.png?raw=true)](https://nosferatu.shawn.dev/) 2 | 3 | [![Nighty Night, Nosferatu](https://github.com/sartak/nighty-night-nosferatu/blob/master/src/assets/gameplay.gif?raw=true)](https://nosferatu.shawn.dev/) 4 | 5 | # Play Live 6 | 7 | [https://nosferatu.shawn.dev/](https://nosferatu.shawn.dev/) 8 | 9 | # Development 10 | 11 | First install the dependencies with `npm install`. 12 | 13 | Run `npm run start`, which should automatically open 14 | [http://localhost:3000](http://localhost:3000). 15 | 16 | The primary game code is in `src/play-scene.js`, with `src/props.js` and 17 | `src/game.js` as supporting files. Assets are under `src/assets/`. 18 | 19 | # Deployment 20 | 21 | Update `package.json` as needed (e.g. for game name, author name, etc). 22 | 23 | Run `npm run build` then put the `build/` directory on a web server. 24 | 25 | To deploy to a location other than `/`, update `homepage` in `package.json`. 26 | 27 | # License 28 | 29 | The MIT License; see `LICENSE.md`. 30 | 31 | -------------------------------------------------------------------------------- /src/scaffolding/Logging.css: -------------------------------------------------------------------------------- 1 | .Logging { 2 | overflow: scroll; 3 | position: fixed; 4 | top: 640px; 5 | left: 32px; 6 | height: 138px; 7 | background: #191919; 8 | border-radius: 8px; 9 | width: 768px; 10 | } 11 | 12 | .Logging ol { 13 | padding: 1em; 14 | margin: 0; 15 | list-style-type: none; 16 | } 17 | 18 | .Logging li { 19 | font-family: "Menlo", "Courier New", monospace; 20 | font-size: 0.7em; 21 | text-indent: -1.5em; 22 | margin: 0 1.5em; 23 | transition: opacity 0.2s linear; 24 | } 25 | 26 | .Logging li + li { 27 | margin-top: 0.5em; 28 | } 29 | 30 | .Logging li .level { 31 | display: none; 32 | } 33 | 34 | .Logging li.debug, 35 | .Logging li.trace { 36 | color: #a0a0a0; 37 | } 38 | 39 | .Logging li.log { 40 | color: #d0d0d0; 41 | } 42 | 43 | .Logging li.info { 44 | color: #4397f7; 45 | } 46 | 47 | .Logging li.recent { 48 | opacity: 0.7; 49 | } 50 | 51 | .Logging li.old { 52 | opacity: 0.4; 53 | transition: opacity 2s linear; 54 | } 55 | 56 | .Logging li:hover { 57 | opacity: 1; 58 | transition: opacity 100ms linear; 59 | } 60 | 61 | .Logging li.warn { 62 | color: #f6c456; 63 | } 64 | 65 | .Logging li.error { 66 | color: #ec5b55; 67 | } 68 | 69 | .Logging li .context::before { 70 | content: " in "; 71 | font-style: italic; 72 | } 73 | 74 | .Logging li .context { 75 | } 76 | -------------------------------------------------------------------------------- /src/scaffolding/Controls.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Controls.css'; 3 | 4 | export default class Controls extends React.Component { 5 | render() { 6 | const { 7 | onMouseMove, volume, onVolumeChange, disableFullscreen, isFullscreen, enterFullscreen, exitFullscreen, 8 | } = this.props; 9 | return ( 10 |
11 |
12 | 13 |    14 | onVolumeChange(e.target.value / 100)} 20 | onMouseUp={(e) => e.target.blur()} 21 | /> 22 |
23 | {disableFullscreen || ( 24 |
25 | {isFullscreen ? ( 26 |
27 |
28 |
29 | ) : ( 30 |
31 |
32 |
33 | )} 34 |
35 | )} 36 |
37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/scaffolding/Development.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Development.css"; 3 | import Engine from "./Engine"; 4 | import Manage from "./Manage"; 5 | import Replay from "./Replay"; 6 | import Logging from "./Logging"; 7 | import { developmentDisplay } from "../../package.json"; 8 | 9 | export default class Development extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | game: null, 15 | activateGame: null, 16 | }; 17 | } 18 | 19 | loadedGame = (game, activateGame) => { 20 | window.game = game; 21 | this.setState({ game, activateGame }); 22 | }; 23 | 24 | render() { 25 | const { game, activateGame } = this.state; 26 | 27 | return ( 28 |
29 | 30 | 31 | 32 | 33 |
34 |
    35 | {developmentDisplay.links.map(([href, label]) => ( 36 |
  • 37 | 38 | {label} 39 | 40 |
  • 41 | ))} 42 |
43 | 44 | 45 | 46 |
47 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFileName = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFileName}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 45 | ) { 46 | // https://github.com/facebook/create-react-app/issues/5210 47 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 48 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 49 | } 50 | 51 | 52 | jest.run(argv); 53 | -------------------------------------------------------------------------------- /src/scaffolding/DoubleEnder.css: -------------------------------------------------------------------------------- 1 | .DoubleEnder { 2 | position: relative; 3 | display: inline-block; 4 | min-width: 100px; 5 | height: 16px; 6 | margin: 2px 4px; 7 | } 8 | 9 | .DoubleEnder .track { 10 | width: 100%; 11 | margin-top: 8px; 12 | height: 4px; 13 | background: black; 14 | border-radius: 4px; 15 | } 16 | 17 | .DoubleEnder .track.selected { 18 | position: absolute; 19 | margin-top: 0; 20 | top: 8px; 21 | z-index: 1; 22 | width: auto; 23 | background: #4397F7; 24 | } 25 | 26 | .DoubleEnder .track.highlighted { 27 | position: absolute; 28 | margin-top: 0; 29 | top: 8px; 30 | z-index: 2; 31 | width: auto; 32 | background: rgb(189, 93, 209); 33 | } 34 | 35 | .DoubleEnder .slider { 36 | position: absolute; 37 | top: 0; 38 | left: 0; 39 | border: 1px solid #000000; 40 | height: 16px; 41 | width: 16px; 42 | border-radius: 16px; 43 | background: #ffffff; 44 | cursor: pointer; 45 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; 46 | } 47 | 48 | .DoubleEnder .slider.dragging { 49 | background: #dddddd; 50 | } 51 | 52 | .DoubleEnder .notch { 53 | display: block; 54 | position: absolute; 55 | top: 2px; 56 | width: 3px; 57 | z-index: 3; 58 | border-radius: 4px; 59 | height: 16px; 60 | background-color: black; 61 | } 62 | 63 | .DoubleEnder .notch.selected { 64 | background: #4397F7; 65 | } 66 | 67 | .DoubleEnder .notch.highlighted { 68 | background: rgb(189, 93, 209); 69 | } 70 | 71 | .DoubleEnder .cursor { 72 | position: absolute; 73 | top: 6px; 74 | width: 2px; 75 | z-index: 6; 76 | height: 8px; 77 | background-color: rgb(189, 93, 209); 78 | } 79 | -------------------------------------------------------------------------------- /src/scaffolding/Engine.css: -------------------------------------------------------------------------------- 1 | body.ld-slate.scaled { 2 | background-color: #000000; 3 | overflow: hidden; 4 | } 5 | 6 | body.natural #engine-container { 7 | width: 800px; 8 | height: 600px; 9 | box-shadow: 0 13px 27px -5px rgba(50, 50, 93, 0.9), 10 | 0 8px 16px -8px rgba(0, 0, 0, 0.95), 0 -6px 16px -6px rgba(0, 0, 0, 0.5); 11 | } 12 | 13 | body.scaled #engine-container { 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | width: 100vw; 18 | height: 100vh; 19 | } 20 | 21 | body.natural #engine-container, 22 | body.natural #engine-container #cover, 23 | body.natural #engine-container canvas { 24 | border-radius: 16px; 25 | } 26 | 27 | .production #engine-container { 28 | margin-top: 32px; 29 | margin-left: auto; 30 | margin-right: auto; 31 | } 32 | 33 | .production #engine-container { 34 | position: relative; 35 | } 36 | 37 | #engine-container.activate { 38 | cursor: pointer; 39 | } 40 | 41 | #engine-container #cover { 42 | position: absolute; 43 | z-index: 1; 44 | width: 800px; 45 | height: 600px; 46 | opacity: 1; 47 | transition: opacity 0.5s linear; 48 | } 49 | 50 | #engine-container.activated #cover { 51 | opacity: 0.5; 52 | } 53 | 54 | #engine-container.activated #lds-spinner { 55 | opacity: 1; 56 | } 57 | 58 | #engine-container.activate #lds-spinner { 59 | animation: 1s linear 0s 1 lds-delaySpinner; 60 | } 61 | 62 | #engine-container { 63 | transition: filter 0.25s; 64 | } 65 | 66 | #engine-container.blurred { 67 | /* filter: brightness(50%); */ 68 | } 69 | 70 | @keyframes lds-delaySpinner { 71 | 0% { 72 | opacity: 0; 73 | } 74 | 90% { 75 | opacity: 0; 76 | } 77 | 100% { 78 | opacity: 1; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/assets/index.js: -------------------------------------------------------------------------------- 1 | // This file is automatically generated by asset scripts in scripts/ 2 | 3 | import dot from "./dot.png"; 4 | import hpbar from "./hpbar.png"; 5 | import player from "./player.png"; 6 | import translucent from "./translucent.png"; 7 | import solid from "./solid.png"; 8 | import crumble from "./crumble.png"; 9 | import button from "./button.png"; 10 | import drawbridge from "./drawbridge.png"; 11 | import drawbridgeAlt from "./drawbridgeAlt.png"; 12 | import smoke from "./smoke.png"; 13 | import bead from "./bead.png"; 14 | import spinner from "./spinner.png"; 15 | import a from "./a.mp3"; 16 | import b from "./b.mp3"; 17 | import c from "./c.mp3"; 18 | import d from "./d.mp3"; 19 | import explode from "./explode.wav"; 20 | import jump1 from "./jump1.wav"; 21 | import jump2 from "./jump2.wav"; 22 | import jump3 from "./jump3.wav"; 23 | import land from "./land.wav"; 24 | import crisp from "./crisp.wav"; 25 | import night from "./night.wav"; 26 | import drawbridgeSound from "./drawbridge.wav"; 27 | import breaking from "./break.wav"; 28 | 29 | export const imageAssets = { 30 | dot, 31 | bead, 32 | smoke, 33 | hpbar, 34 | button, 35 | drawbridge, 36 | drawbridgeAlt, 37 | spinner, 38 | translucent, 39 | solid, 40 | }; 41 | 42 | export const spriteAssets = { 43 | player: { 44 | file: player, 45 | frameWidth: 32, 46 | frameHeight: 32, 47 | }, 48 | crumble: { 49 | file: crumble, 50 | frameWidth: 2 + 24 * 3, 51 | frameHeight: 24, 52 | }, 53 | }; 54 | 55 | export const musicAssets = { 56 | a, 57 | b, 58 | c, 59 | d, 60 | }; 61 | 62 | export const soundAssets = { 63 | explode, 64 | jump1, 65 | jump2, 66 | jump3, 67 | land, 68 | crisp, 69 | night, 70 | breaking, 71 | drawbridge: drawbridgeSound, 72 | }; 73 | -------------------------------------------------------------------------------- /src/scaffolding/lib/camera.js: -------------------------------------------------------------------------------- 1 | import SimplexNoise from 'simplex-noise'; 2 | import prop from '../../props'; 3 | 4 | const noiseX = new SimplexNoise('X'); 5 | const noiseY = new SimplexNoise('Y'); 6 | const noiseT = new SimplexNoise('T'); 7 | 8 | let injected = false; 9 | export function injectCameraShake(camera) { 10 | if (injected || !camera) { 11 | return injected; 12 | } 13 | 14 | // eslint-disable-next-line no-proto 15 | const proto = camera.__proto__; 16 | 17 | const origPreRender = proto.preRender; 18 | 19 | proto.preRender = function(...args) { 20 | origPreRender.call(this, ...args); 21 | 22 | if (prop('scene.trauma.legacy') || !prop('scene.trauma.enabled')) { 23 | return; 24 | } 25 | 26 | const {width, height, scene} = this; 27 | const { 28 | _traumaShake, time, timeScale, _traumaStart, 29 | } = scene; 30 | const {now} = time; 31 | 32 | if (!_traumaShake) { 33 | return; 34 | } 35 | 36 | const delta = now - _traumaStart; 37 | const t = delta * prop('scene.trauma.speed') / timeScale ** 2; 38 | const easeIn = Math.min(1, delta / (prop('scene.trauma.easeIn') * timeScale)); 39 | 40 | const dx = easeIn * _traumaShake * prop('scene.trauma.dx') * noiseX.noise2D(_traumaStart, t); 41 | const dy = easeIn * _traumaShake * prop('scene.trauma.dy') * noiseY.noise2D(_traumaStart, t); 42 | const dt = easeIn * _traumaShake * prop('scene.trauma.dt') * noiseT.noise2D(_traumaStart, t); 43 | 44 | const halfWidth = width / 2; 45 | const halfHeight = height / 2; 46 | 47 | // rotate about the center 48 | this.matrix.translate(halfWidth, halfHeight); 49 | this.matrix.rotate(dt); 50 | this.matrix.translate(-halfWidth, -halfHeight); 51 | 52 | this.matrix.translate(dx, dy); 53 | }; 54 | 55 | injected = true; 56 | 57 | return injected; 58 | } 59 | -------------------------------------------------------------------------------- /scripts/watch-sync.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use Filesys::Notify::Simple; 5 | use File::Spec; 6 | 7 | @ARGV == 2 || @ARGV == 3 or die "usage: $0 src dest [verbose]"; 8 | 9 | my $source = File::Spec->rel2abs(shift); 10 | my $destination = shift; 11 | my $verbose = shift; 12 | 13 | my @excludes = ( 14 | "build/", 15 | # "node_modules/", 16 | # ".git/", 17 | ); 18 | 19 | my $watcher = Filesys::Notify::Simple->new([$source]); 20 | 21 | my @ignore = map { File::Spec->catdir($source, $_) } @excludes; 22 | 23 | if (my $syncpid = fork) { 24 | while (1) { 25 | $watcher->wait(sub { 26 | for (@_) { 27 | my $path = $_->{path}; 28 | next if grep { rindex($path, $_, 0) == 0 } @ignore; 29 | 30 | warn "Syncing because file changed: " . File::Spec->abs2rel($path) . "\n" if $verbose; 31 | kill 'HUP', $syncpid; 32 | last; 33 | } 34 | }); 35 | } 36 | } 37 | else { 38 | my @command = ( 39 | "rsync", 40 | "--info=stats1", 41 | "--delete", 42 | "-az", 43 | (map { ("--exclude", $_) } @excludes), 44 | $source, 45 | $destination, 46 | ); 47 | 48 | warn "Initial sync\n" 49 | if $verbose; 50 | system(@command); 51 | 52 | my $repeat = 0; 53 | while (1) { 54 | eval { 55 | local $SIG{HUP} = sub { 56 | $repeat = 1; 57 | warn "Scheduled fast followup sync\n" 58 | if $verbose; 59 | }; 60 | 61 | unless ($repeat) { 62 | eval { 63 | local $SIG{HUP} = sub { die "HUP\n"; }; 64 | sleep; 65 | }; 66 | 67 | die $@ unless $@ eq "HUP\n"; 68 | } 69 | 70 | $repeat = 0; 71 | 72 | warn "Syncing…\n" 73 | if $verbose; 74 | system(@command); 75 | warn "Sync complete\n" 76 | if $verbose; 77 | }; 78 | 79 | die $@ if $@; 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/scaffolding/lib/store.js: -------------------------------------------------------------------------------- 1 | import {name as project} from '../../../package.json'; 2 | 3 | const debug = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; 4 | const prefix = `${project}_${debug ? 'debug' : 'prod'}_`; 5 | 6 | let frozen = false; 7 | export function freezeStorage() { 8 | frozen = true; 9 | } 10 | 11 | export function saveField(name, value) { 12 | if (frozen) { 13 | return false; 14 | } 15 | 16 | try { 17 | const payload = JSON.stringify(value); 18 | localStorage.setItem(`${prefix}${name}`, payload); 19 | } catch (e) { 20 | // eslint-disable-next-line no-console 21 | console.error(e); 22 | return false; 23 | } 24 | 25 | return true; 26 | } 27 | 28 | export function removeField(name) { 29 | if (frozen) { 30 | return false; 31 | } 32 | 33 | try { 34 | localStorage.removeItem(`${prefix}${name}`); 35 | } catch (e) { 36 | // eslint-disable-next-line no-console 37 | console.error(e); 38 | return false; 39 | } 40 | 41 | return true; 42 | } 43 | 44 | export function removeAllFields(subprefix = '') { 45 | if (frozen) { 46 | return false; 47 | } 48 | 49 | try { 50 | Object.keys(localStorage).forEach((key) => { 51 | if (key.startsWith(`${prefix}${subprefix}`)) { 52 | localStorage.removeItem(key); 53 | } 54 | }); 55 | } catch (e) { 56 | // eslint-disable-next-line no-console 57 | console.error(e); 58 | return false; 59 | } 60 | 61 | return true; 62 | } 63 | 64 | export function loadField(name, defaultValue) { 65 | let value = defaultValue; 66 | 67 | try { 68 | const payload = localStorage.getItem(`${prefix}${name}`); 69 | if (payload !== null && payload !== undefined) { 70 | value = JSON.parse(payload); 71 | } 72 | } catch (e) { 73 | // eslint-disable-next-line no-console 74 | console.error(e); 75 | } 76 | 77 | if (typeof value === 'function') { 78 | value = value(); 79 | } 80 | 81 | return value; 82 | } 83 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | %GAME_NAME% 24 | 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/scaffolding/Manage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Manage.css'; 3 | import {saveField, loadField} from './lib/store'; 4 | import { 5 | initializeManage, 6 | updateSearch, 7 | serializeChangedProps, 8 | resetChangedProps, 9 | } from './lib/manage-gui'; 10 | 11 | export default class Manage extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | search: loadField('search', ''), 17 | }; 18 | 19 | this.ref = React.createRef(); 20 | } 21 | 22 | componentDidMount() { 23 | const {search} = this.state; 24 | 25 | const gui = initializeManage(); 26 | this.ref.current.append(gui.domElement); 27 | 28 | if (search !== '') { 29 | updateSearch(search, true); 30 | } 31 | } 32 | 33 | copyChangedProps() { 34 | const tempNode = document.createElement('textarea'); 35 | tempNode.value = serializeChangedProps(); 36 | document.body.appendChild(tempNode); 37 | tempNode.select(); 38 | document.execCommand('copy'); 39 | document.body.removeChild(tempNode); 40 | } 41 | 42 | render() { 43 | const {search} = this.state; 44 | return ( 45 |
46 | { 52 | // eslint-disable-next-line react/destructuring-assignment 53 | const isStarted = this.state.search === ''; 54 | this.setState({search: e.target.value}); 55 | updateSearch(e.target.value, isStarted); 56 | saveField('search', e.target.value); 57 | }} 58 | /> 59 | this.copyChangedProps()} 64 | /> 65 | resetChangedProps()} 70 | /> 71 |
72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/scaffolding/base.css: -------------------------------------------------------------------------------- 1 | body.ld-slate { 2 | margin: 0; 3 | padding: 0; 4 | background-color: #efefff; 5 | font-family: "Avenir Next", "Avenir", "Helvetica Neue", "Helvetica", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | image-rendering: pixelated; 10 | } 11 | 12 | #lds-spinner { 13 | position: absolute; 14 | width: 100%; 15 | height: 100%; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | .lds-spinner { 22 | display: inline-block; 23 | position: relative; 24 | width: 64px; 25 | height: 64px; 26 | } 27 | .lds-spinner div { 28 | transform-origin: 32px 32px; 29 | animation: lds-spinner 1.2s linear infinite; 30 | } 31 | .lds-spinner div:after { 32 | content: " "; 33 | display: block; 34 | position: absolute; 35 | top: 3px; 36 | left: 29px; 37 | width: 5px; 38 | height: 14px; 39 | border-radius: 20%; 40 | background: #fff; 41 | } 42 | .lds-spinner div:nth-child(1) { 43 | transform: rotate(0deg); 44 | animation-delay: -1.1s; 45 | } 46 | .lds-spinner div:nth-child(2) { 47 | transform: rotate(30deg); 48 | animation-delay: -1s; 49 | } 50 | .lds-spinner div:nth-child(3) { 51 | transform: rotate(60deg); 52 | animation-delay: -0.9s; 53 | } 54 | .lds-spinner div:nth-child(4) { 55 | transform: rotate(90deg); 56 | animation-delay: -0.8s; 57 | } 58 | .lds-spinner div:nth-child(5) { 59 | transform: rotate(120deg); 60 | animation-delay: -0.7s; 61 | } 62 | .lds-spinner div:nth-child(6) { 63 | transform: rotate(150deg); 64 | animation-delay: -0.6s; 65 | } 66 | .lds-spinner div:nth-child(7) { 67 | transform: rotate(180deg); 68 | animation-delay: -0.5s; 69 | } 70 | .lds-spinner div:nth-child(8) { 71 | transform: rotate(210deg); 72 | animation-delay: -0.4s; 73 | } 74 | .lds-spinner div:nth-child(9) { 75 | transform: rotate(240deg); 76 | animation-delay: -0.3s; 77 | } 78 | .lds-spinner div:nth-child(10) { 79 | transform: rotate(270deg); 80 | animation-delay: -0.2s; 81 | } 82 | .lds-spinner div:nth-child(11) { 83 | transform: rotate(300deg); 84 | animation-delay: -0.1s; 85 | } 86 | .lds-spinner div:nth-child(12) { 87 | transform: rotate(330deg); 88 | animation-delay: 0s; 89 | } 90 | @keyframes lds-spinner { 91 | 0% { 92 | opacity: 1; 93 | } 94 | 100% { 95 | opacity: 0; 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | 8 | /** 9 | * Get the baseUrl of a compilerOptions object. 10 | * 11 | * @param {Object} options 12 | */ 13 | function getAdditionalModulePaths(options = {}) { 14 | const baseUrl = options.baseUrl; 15 | 16 | // We need to explicitly check for null and undefined (and not a falsy value) because 17 | // TypeScript treats an empty string as `.`. 18 | if (baseUrl == null) { 19 | // If there's no baseUrl set we respect NODE_PATH 20 | // Note that NODE_PATH is deprecated and will be removed 21 | // in the next major release of create-react-app. 22 | 23 | const nodePath = process.env.NODE_PATH || ''; 24 | return nodePath.split(path.delimiter).filter(Boolean); 25 | } 26 | 27 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 28 | 29 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 30 | // the default behavior. 31 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 32 | return null; 33 | } 34 | 35 | // Allow the user set the `baseUrl` to `appSrc`. 36 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 37 | return [paths.appSrc]; 38 | } 39 | 40 | // Otherwise, throw an error. 41 | throw new Error( 42 | chalk.red.bold( 43 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 44 | ' Create React App does not support other values at this time.' 45 | ) 46 | ); 47 | } 48 | 49 | function getModules() { 50 | // Check if TypeScript is setup 51 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 52 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 53 | 54 | if (hasTsConfig && hasJsConfig) { 55 | throw new Error( 56 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 57 | ); 58 | } 59 | 60 | let config; 61 | 62 | // If there's a tsconfig.json we assume it's a 63 | // TypeScript project and set up the config 64 | // based on tsconfig.json 65 | if (hasTsConfig) { 66 | config = require(paths.appTsConfig); 67 | // Otherwise we'll check if there is jsconfig.json 68 | // for non TS projects. 69 | } else if (hasJsConfig) { 70 | config = require(paths.appJsConfig); 71 | } 72 | 73 | config = config || {}; 74 | const options = config.compilerOptions || {}; 75 | 76 | const additionalModulePaths = getAdditionalModulePaths(options); 77 | 78 | return { 79 | additionalModulePaths: additionalModulePaths, 80 | hasTsConfig, 81 | }; 82 | } 83 | 84 | module.exports = getModules(); 85 | -------------------------------------------------------------------------------- /scripts/add-asset.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use lib 'scripts'; 5 | use Assets; 6 | use File::Find; 7 | use File::Spec; 8 | 9 | (@ARGV == 1 && $ARGV[0] eq 'auto') || @ARGV == 2 || @ARGV == 3 or die "usage: $0 name type [path]\n or: $0 auto\n"; 10 | my ($name, $type, $path) = @ARGV; 11 | my $auto = $name eq 'auto' && @ARGV == 1; 12 | 13 | die "malformed name $name, expected identifier-style" unless $name =~ /^[A-Za-z_][A-Za-z_0-9]*$/; 14 | 15 | if (!$auto) { 16 | $type = canonicalize_asset_type($type); 17 | } 18 | 19 | my $assets_file = 'src/assets/index.js'; 20 | my $assets = parse_assets($assets_file); 21 | 22 | die "no file at $path" if $path && !-e $path; 23 | 24 | my @new_files; 25 | if (!$path) { 26 | my %seen_files = map { $_->{path} => 1 } values %$assets; 27 | $seen_files{"./ld-cover.png"} = 1; 28 | $seen_files{"./cover.png"} = 1; 29 | $seen_files{"./uncropped-cover.png"} = 1; 30 | 31 | find(sub { 32 | return if -d $_; 33 | return if /^\./; 34 | $File::Find::name =~ s!^src/assets/!./!; 35 | return if $File::Find::name =~ m{^\./public/}; 36 | return if $seen_files{$File::Find::name}; 37 | return unless /\.(jpg|png|wav|mp3)/; 38 | return if -e ".$_.ignore"; 39 | push @new_files, $File::Find::name; 40 | }, 'src/assets/'); 41 | } 42 | 43 | my @add_paths; 44 | 45 | if ($auto) { 46 | for my $path (@new_files) { 47 | my $type; 48 | my $name = do { 49 | my $tmp = $path; 50 | $tmp =~ s/\.(\w+)$//; 51 | my $extension = $1; 52 | if ($extension eq 'mp3') { 53 | $type = 'musicAssets'; 54 | } 55 | elsif ($extension eq 'wav') { 56 | $type = 'soundAssets'; 57 | } 58 | elsif ($extension eq 'png' || $extension eq 'jpg') { 59 | $type = $path =~ /sprite/i ? 'spriteAssets' : 'imageAssets'; 60 | } 61 | 62 | lcfirst join '', map { ucfirst lc } grep { length && $_ ne '.' } split qr![-/]!, $tmp; 63 | }; 64 | 65 | die "unable to intuit name for $path" if !$name; 66 | die "unable to intuit type for $path" if !$type; 67 | 68 | push @add_paths, File::Spec->catfile('src', 'assets', $path); 69 | $assets->{$name} = { 70 | path => $path, 71 | type => $type, 72 | }; 73 | } 74 | } 75 | else { 76 | die "already asset named $name" if $assets->{$name}; 77 | 78 | if ($path) { 79 | $path =~ s!^src/assets/!./!; 80 | } 81 | else { 82 | my @candidates = grep { is_candidate_for_type($_, $type) } @new_files; 83 | die "expected exactly 1 $type candidate file under src/assets/, got " . (@candidates ? join ", ", @candidates : "none") . ". specify path as a third parameter" if @candidates != 1; 84 | $path = $candidates[0]; 85 | } 86 | 87 | push @add_paths, File::Spec->catfile('src', 'assets', $path); 88 | $assets->{$name} = { 89 | path => $path, 90 | type => $type, 91 | }; 92 | } 93 | 94 | emit_and_diff_assets($assets_file, $assets); 95 | if (@add_paths) { 96 | system("git", "add", @add_paths); 97 | } 98 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 226 | 227 | 228 |
229 | START 230 | 231 | my $handle; 232 | open $handle, '<', $logFile 233 | or die $!; 234 | 235 | my @buf; 236 | my $bufd; 237 | sub get { 238 | return @{ shift @buf } if @buf; 239 | while (<$handle>) { 240 | next if /launch/; 241 | /^(\d+(?:\.\d*)?) (\w)$/ or die "invalid line $_"; 242 | $bufd ||= $1; 243 | my @ret = ($1, $2, $1 - $bufd); 244 | $bufd = $1; 245 | return @ret; 246 | } 247 | return; 248 | } 249 | sub unget { unshift @buf, [@_] } 250 | 251 | my $s = $start; 252 | my $key = 'd'; 253 | 254 | my @m; 255 | 256 | for my $h (0..48) { 257 | for my $m (0..59) { 258 | $s += 60; 259 | my $e = $s + 60; 260 | 261 | $key = 'u'; 262 | 263 | my @relevant; 264 | while (1) { 265 | my ($t, $mode, $duration) = get(); 266 | 267 | if (!$t) { 268 | $key = 'u'; 269 | last; 270 | } 271 | 272 | if ($t > $e) { 273 | unget($t, $mode, $duration); 274 | last; 275 | } 276 | 277 | if ($t < $s) { 278 | next; 279 | } 280 | 281 | push @relevant, [$t, $mode, $duration]; 282 | } 283 | 284 | if (@relevant) { 285 | my %t; 286 | for (@relevant) { 287 | my ($t, $mode, $duration) = @$_; 288 | $t{$mode} += $duration; 289 | } 290 | 291 | my $top_mode = 'u'; 292 | my $top_time = 1e9; 293 | for my $mode (keys %t) { 294 | if ($t{$mode} < $top_time) { 295 | $top_time = $t{$mode}; 296 | $top_mode = $mode; 297 | } 298 | } 299 | 300 | $key = $top_mode; 301 | } 302 | 303 | push @m, $key; 304 | } 305 | } 306 | 307 | #@m = map { $cats[rand @cats] } @m; 308 | 309 | #for my $key (@m) { 310 | # print qq[
]; 311 | #} 312 | 313 | for my $cat (@cats) { 314 | print qq[
]; 315 | 316 | for my $h (0..48) { 317 | for my $i (0..19) { 318 | for my $j (0..2) { 319 | my $k = $h*60+$j*20+$i; 320 | my $key = $m[$k]; 321 | my $x = $h*3 + $j; 322 | my $y = $i; 323 | next if $key ne $cat; 324 | print qq[
]; 325 | } 326 | } 327 | } 328 | 329 | print qq[
]; 330 | } 331 | 332 | print "
\n"; 333 | print "
    \n"; 334 | for (@categories) { 335 | my ($key, $name, $color) = @$_; 336 | print qq[
  • \n]; 342 | } 343 | 344 | print << "START"; 345 |
346 |
347 | 348 | 349 | 470 | 471 | END 472 | -------------------------------------------------------------------------------- /src/scaffolding/lib/shaders.js: -------------------------------------------------------------------------------- 1 | export const builtinCoordFragments = [ 2 | ['shockwave', { 3 | time: ['float', 0, null], 4 | center: ['vec2', [0.5, 0.5], null], 5 | scale: ['float', 10.0, 0, 500], 6 | range: ['float', 0.8, 0, 10], 7 | thickness: ['float', 0.1, 0, 10], 8 | speed: ['float', 3.0, 0, 50], 9 | inner: ['float', 0.09, 0, 1], 10 | dropoff: ['float', 40.0, 0, 500], 11 | }, ` 12 | float shockwave_dt = (scene_time - shockwave_time) / 3333.0; 13 | if (shockwave_time > 0.0 && shockwave_dt < 10.0) { 14 | float dist = distance(uv, shockwave_center - camera_scroll); 15 | float t = shockwave_dt * shockwave_speed; 16 | if (dist <= t + shockwave_thickness && dist >= t - shockwave_thickness && dist >= shockwave_inner) { 17 | float diff = dist - t; 18 | float scaleDiff = 1.0 - pow(abs(diff * shockwave_scale), shockwave_range); 19 | float diffTime = diff * scaleDiff; 20 | vec2 diffTexCoord = normalize(uv - (shockwave_center - camera_scroll)); 21 | uv += (diffTexCoord * diffTime) / (t * dist * shockwave_dropoff); 22 | } 23 | } 24 | `], 25 | ]; 26 | 27 | export const builtinColorFragments = [ 28 | ['blur', { 29 | amount: ['float', 0, null], 30 | }, ` 31 | if (blur_amount > 0.0) { 32 | float b = blur_amount / resolution.x; 33 | c *= 0.2270270270; 34 | c += texture2D(uMainSampler, vec2(uv.x - 4.0*b, uv.y - 4.0*b)) * 0.0162162162; 35 | c += texture2D(uMainSampler, vec2(uv.x - 3.0*b, uv.y - 3.0*b)) * 0.0540540541; 36 | c += texture2D(uMainSampler, vec2(uv.x - 2.0*b, uv.y - 2.0*b)) * 0.1216216216; 37 | c += texture2D(uMainSampler, vec2(uv.x - 1.0*b, uv.y - 1.0*b)) * 0.1945945946; 38 | c += texture2D(uMainSampler, vec2(uv.x + 1.0*b, uv.y + 1.0*b)) * 0.1945945946; 39 | c += texture2D(uMainSampler, vec2(uv.x + 2.0*b, uv.y + 2.0*b)) * 0.1216216216; 40 | c += texture2D(uMainSampler, vec2(uv.x + 3.0*b, uv.y + 3.0*b)) * 0.0540540541; 41 | c += texture2D(uMainSampler, vec2(uv.x + 4.0*b, uv.y + 4.0*b)) * 0.0162162162; 42 | } 43 | `], 44 | 45 | ['aberration', { 46 | red: ['vec2', [0, 0], null], 47 | green: ['vec2', [0, 0], null], 48 | blue: ['vec2', [0, 0], null], 49 | }, ` 50 | c.r += texture2D(uMainSampler, vec2(uv.x - aberration_red.x, uv.y - aberration_red.y)).r; 51 | c.r -= texture2D(uMainSampler, vec2(uv.x + aberration_red.x, uv.y + aberration_red.y)).r; 52 | 53 | c.g += texture2D(uMainSampler, vec2(uv.x - aberration_green.x, uv.y - aberration_green.y)).g; 54 | c.g -= texture2D(uMainSampler, vec2(uv.x + aberration_green.x, uv.y + aberration_green.y)).g; 55 | 56 | c.b += texture2D(uMainSampler, vec2(uv.x - aberration_blue.x, uv.y - aberration_blue.y)).b; 57 | c.b -= texture2D(uMainSampler, vec2(uv.x + aberration_blue.x, uv.y + aberration_blue.y)).b; 58 | `], 59 | 60 | ['tint', { 61 | color: ['rgba', [1, 1, 1, 1]], 62 | }, ` 63 | c.r *= tint_color.r * tint_color.a; 64 | c.g *= tint_color.g * tint_color.a; 65 | c.b *= tint_color.b * tint_color.a; 66 | `], 67 | ]; 68 | 69 | export const shaderTypeMeta = { 70 | float: [1, 'float', 'setFloat1'], 71 | bool: [1, 'float', 'setFloat1'], 72 | vec2: [2, 'vec2', 'setFloat2v', 'x', 'y'], 73 | vec3: [3, 'vec3', 'setFloat3v', 'x', 'y', 'z'], 74 | vec4: [4, 'vec4', 'setFloat4v', 'x', 'y', 'z', 'w'], 75 | rgb: [3, 'vec3', 'setFloat3v', 'r', 'g', 'b'], 76 | rgba: [4, 'vec4', 'setFloat4v', 'r', 'g', 'b', 'a'], 77 | }; 78 | 79 | export function propNamesForUniform(fragmentName, uniformName, spec) { 80 | let [type] = spec; 81 | 82 | if (!type) { 83 | type = 'float'; 84 | } 85 | 86 | const [count, , , ...subvariables] = shaderTypeMeta[type]; 87 | 88 | if (type === 'rgb') { 89 | let sub = ''; 90 | if (!uniformName.match(/color$/i)) { 91 | sub = '_color'; 92 | } 93 | 94 | return [`shader.${fragmentName}.${uniformName}${sub}`]; 95 | } else if (type === 'rgba') { 96 | return [ 97 | `shader.${fragmentName}.${uniformName}_color`, 98 | `shader.${fragmentName}.${uniformName}_alpha`, 99 | ]; 100 | } else if (count === 1) { 101 | return [`shader.${fragmentName}.${uniformName}`]; 102 | } else { 103 | return subvariables.map((sub, i) => { 104 | return `shader.${fragmentName}.${uniformName}_${sub}`; 105 | }); 106 | } 107 | } 108 | 109 | function injectBuiltinFragment(fragments, isCoord) { 110 | let primary = builtinColorFragments; 111 | let secondary = builtinCoordFragments; 112 | let primaryName = 'shaderColorFragments'; 113 | let secondaryName = 'shaderCoordFragments'; 114 | 115 | if (!fragments) { 116 | return []; 117 | } 118 | 119 | if (isCoord) { 120 | [primary, secondary] = [secondary, primary]; 121 | [primaryName, secondaryName] = [secondaryName, primaryName]; 122 | } 123 | 124 | if (fragments.length === 0) { 125 | fragments.push(...primary); 126 | return; 127 | } 128 | 129 | for (let i = 0; i < fragments.length; i += 1) { 130 | if (typeof fragments[i] === 'string') { 131 | const name = fragments[i]; 132 | const replacement = primary.find(([p]) => name === p); 133 | if (replacement) { 134 | fragments[i] = replacement; 135 | } else { 136 | // eslint-disable-next-line no-console 137 | console.error(`Unable to find builtin ${primaryName} '${name}'; available are: ${primary.map(([p]) => p).join(', ')}`); 138 | 139 | const suggestion = secondary.find(([p]) => name === p); 140 | if (suggestion) { 141 | // eslint-disable-next-line no-console 142 | console.error(`Perhaps you meant the builtin ${secondaryName} '${name}'?`); 143 | } 144 | 145 | fragments.splice(i, 1); 146 | i -= 1; 147 | } 148 | } 149 | } 150 | } 151 | 152 | export function shaderProps(coordFragments, colorFragments) { 153 | const props = {}; 154 | 155 | injectBuiltinFragment(coordFragments, true); 156 | injectBuiltinFragment(colorFragments, false); 157 | 158 | [...(coordFragments || []), ...(colorFragments || [])].forEach(([fragmentName, uniforms]) => { 159 | props[`shader.${fragmentName}.enabled`] = [true, (value, scene, game) => game.recompileShaders()]; 160 | 161 | Object.entries(uniforms).forEach(([uniformName, spec]) => { 162 | // eslint-disable-next-line prefer-const 163 | let [type, ...config] = spec; 164 | 165 | const name = `${fragmentName}_${uniformName}`; 166 | 167 | if (!type) { 168 | type = 'float'; 169 | } 170 | 171 | if (!shaderTypeMeta[type]) { 172 | throw new Error(`Unknown type ${type} for shader ${name}`); 173 | } 174 | 175 | const [count, , setter, ...subvariables] = shaderTypeMeta[type]; 176 | 177 | if (uniformName.match(/color$/i) && type !== 'rgb' && type !== 'rgba') { 178 | throw new Error(`Shader uniform ${name} ends with /color$/i but it isn't using type rgb or rgba`); 179 | } 180 | 181 | if (type === 'rgb') { 182 | if (config.length > 2 183 | || config.length === 0 184 | || !Array.isArray(config[0]) 185 | || config[0].length !== 3 186 | || (config.length === 2 && config[1] !== null)) { 187 | throw new Error(`Expected rgb shader uniform ${name} to have shape ['rgb', [0.95, 0.25, 0.5]] or ['rgb', [0.95, 0.25, 0.5], null]`); 188 | } 189 | 190 | let sub = ''; 191 | if (!uniformName.match(/color$/i)) { 192 | sub = '_color'; 193 | } 194 | 195 | if (config[1] === null) { 196 | config.push((scene) => (scene[name] ? scene[name].map((c) => c * 255.0) : undefined)); 197 | } else { 198 | config.push((_, scene, game) => { 199 | if (!scene.shader) { 200 | return; 201 | } 202 | const value = game.prop(`shader.${fragmentName}.${uniformName}${sub}`).map((c) => c / 255.0); 203 | scene.shader[setter](name, value); 204 | }); 205 | } 206 | 207 | config[0] = config[0].map((c) => c * 255.0); 208 | props[`shader.${fragmentName}.${uniformName}${sub}`] = config; 209 | } else if (type === 'rgba') { 210 | if (config.length > 2 211 | || config.length === 0 212 | || !Array.isArray(config[0]) 213 | || config[0].length !== 4 214 | || (config.length === 2 && config[1] !== null)) { 215 | throw new Error(`Expected rgbs shader uniform ${name} to have shape ['rgba', [0.95, 0.25, 0.5, 1]] or ['rgb', [0.95, 0.25, 0.5, 1], null]`); 216 | } 217 | 218 | const colorConfig = [config[0].filter((_, i) => i < 3)]; 219 | const alphaConfig = [config[0][3]]; 220 | 221 | if (config[1] === null) { 222 | colorConfig.push(null); 223 | alphaConfig.push(null); 224 | 225 | colorConfig.push((scene) => (scene[name] ? scene[name].filter((_, i) => i < 3).map((c) => c * 255.0) : undefined)); 226 | alphaConfig.push((scene) => (scene[name] ? scene[name][3] : undefined)); 227 | } else { 228 | alphaConfig.push(0, 1); // min and max 229 | 230 | const cb = (value, scene, game) => { 231 | if (!scene.shader) { 232 | return; 233 | } 234 | 235 | scene.shader[setter](name, [ 236 | ...game.prop(`shader.${fragmentName}.${uniformName}_color`).map((c) => c / 255.0), 237 | game.prop(`shader.${fragmentName}.${uniformName}_alpha`), 238 | ]); 239 | }; 240 | colorConfig.push(cb); 241 | alphaConfig.push(cb); 242 | } 243 | 244 | colorConfig[0] = colorConfig[0].map((c) => c * 255.0); 245 | props[`shader.${fragmentName}.${uniformName}_color`] = colorConfig; 246 | props[`shader.${fragmentName}.${uniformName}_alpha`] = alphaConfig; 247 | } else if (type === 'bool') { 248 | if (config[1] === null) { 249 | config.push((scene) => scene[name]); 250 | } else if (typeof config[config.length - 1] !== 'function') { 251 | config.push((value, scene) => scene.shader && scene.shader[setter](name, value ? 1.0 : 0.0)); 252 | } 253 | 254 | props[`shader.${fragmentName}.${uniformName}`] = config; 255 | } else if (count === 1) { 256 | if (config[1] === null) { 257 | config.push((scene) => scene[name]); 258 | } else if (typeof config[config.length - 1] !== 'function') { 259 | config.push((value, scene) => scene.shader && scene.shader[setter](name, value)); 260 | } 261 | 262 | if (config[0] === 0 && config[1] === null) { 263 | config[0] = 0.1; 264 | } 265 | 266 | props[`shader.${fragmentName}.${uniformName}`] = config; 267 | } else { 268 | subvariables.forEach((sub, i) => { 269 | const c = [...config]; 270 | c[0] = c[0][i]; 271 | 272 | if (c[1] === null) { 273 | c.push((scene) => (scene[name] ? scene[name][i] : undefined)); 274 | } else if (typeof c[c.length - 1] !== 'function') { 275 | c.push((_, scene, game) => { 276 | if (!scene.shader) { 277 | return; 278 | } 279 | 280 | const value = subvariables.map((s) => game.prop(`shader.${fragmentName}.${uniformName}_${s}`)); 281 | scene.shader[setter](name, value); 282 | }); 283 | } 284 | 285 | if (c[0] === 0 && c[1] === null) { 286 | c[0] = 0.1; 287 | } 288 | 289 | props[`shader.${fragmentName}.${uniformName}_${sub}`] = c; 290 | }); 291 | } 292 | }); 293 | }); 294 | 295 | return props; 296 | } 297 | -------------------------------------------------------------------------------- /src/assets/maps.txt: -------------------------------------------------------------------------------- 1 | ............................... 2 | ............................... 3 | ............................... 4 | ..............I................ 5 | ..............I................ 6 | ..............I................ 7 | ..............I................ 8 | ..............I................ 9 | ..............I................ 10 | ..............I................ 11 | ............................... 12 | .....................####...... 13 | ...I........................... 14 | ...I........................... 15 | ...I........................... 16 | ...I........................... 17 | ...I..............####......... 18 | ...I........................... 19 | ...I....####................... 20 | ...I........................... 21 | ...I........................... 22 | ...I............@.............. 23 | ...I_________________________.. 24 | 25 | { 26 | "id": "tutorial", 27 | "music": "a", 28 | "sunSpeed": 30, 29 | "hi": "The wretched dawn . . .", 30 | "bye": "The soothing embrace of night . . ." 31 | } 32 | 33 | ............................... 34 | ............................... 35 | ............................... 36 | ............................... 37 | ............................... 38 | ............................... 39 | .............../......\........ 40 | ............................... 41 | ............................... 42 | .........................I..... 43 | ............................... 44 | .........................I..... 45 | ............................... 46 | .........................I..... 47 | .....III....................... 48 | .........................I..... 49 | ............................... 50 | ............................... 51 | ...I........................I.. 52 | ...I........................I.. 53 | ...I........................I.. 54 | ...I......@.................I.. 55 | ...I________________________I.. 56 | 57 | { 58 | "id": "blart", 59 | "music": "a", 60 | "sunSpeed": 19, 61 | "hi": "It passes so quickly . . .", 62 | "bye": "At best all I can do is . . ." 63 | } 64 | 65 | ............................... 66 | ............................... 67 | ............................... 68 | ............................... 69 | ............................... 70 | ............................... 71 | ....................../........ 72 | .....@......................... 73 | ...IIII........................ 74 | ............................... 75 | ............................... 76 | ............................... 77 | ............................... 78 | ......IIII..................... 79 | ............................... 80 | .......................\....... 81 | ............................... 82 | ..IIII......................... 83 | ............................... 84 | ............................... 85 | ............................... 86 | ............................... 87 | ............................... 88 | 89 | { 90 | "id": "yerp", 91 | "music": "a", 92 | "sunSpeed": 50, 93 | "hi": "Delay the inevitable.", 94 | "bye": "" 95 | } 96 | 97 | ............................... 98 | ............................... 99 | ............................... 100 | ............................... 101 | ............................... 102 | ............................... 103 | ..................I............ 104 | ............................... 105 | ..........I.................... 106 | ............................... 107 | ..............I.......I........ 108 | .......I....................... 109 | ......I.................I...... 110 | ............................... 111 | ............................... 112 | .........................I..... 113 | ............................... 114 | ..I............................ 115 | ............................I.. 116 | ..I.........................I.. 117 | ............................I.. 118 | ..I......@..................I.. 119 | ......_____....______.......I.. 120 | 121 | { 122 | "id": "yas", 123 | "music": "a", 124 | "sunSpeed": 50, 125 | "hi": "", 126 | "bye": "" 127 | } 128 | 129 | ............................... 130 | ............................... 131 | ............................... 132 | ............................... 133 | ............................... 134 | .....................I......... 135 | .....................I......... 136 | .....................I......... 137 | ............................... 138 | ............................... 139 | ....I.......III................ 140 | ....I.....................I.... 141 | ....I.....................I.... 142 | ............................... 143 | ............................... 144 | ........~...................... 145 | .I............................. 146 | .I............................. 147 | .I............~................ 148 | .I............................. 149 | .I............................. 150 | .I..................@.......... 151 | .I...___________________....... 152 | 153 | { 154 | "id": "crumble", 155 | "music": "b", 156 | "sunSpeed": 30, 157 | "hi": "These weary stones offer no respite.", 158 | "bye": "" 159 | } 160 | 161 | ............................... 162 | ............................... 163 | ............................... 164 | ............................... 165 | ............................... 166 | ............................... 167 | ............................... 168 | ............................... 169 | .............~.....~........... 170 | ............................... 171 | ............................... 172 | ............................... 173 | ........~.....\................ 174 | ............................... 175 | ............................... 176 | ............................... 177 | ............................... 178 | ..I..~................III...... 179 | ..I............................ 180 | ..I............................ 181 | ..I............................ 182 | ..I......@..................... 183 | ..I..._____....______.......... 184 | 185 | { 186 | "id": "missthespinner", 187 | "music": "b", 188 | "sunSpeed": 50, 189 | "hi": "", 190 | "bye": "" 191 | } 192 | 193 | ............................... 194 | ............................... 195 | ............................... 196 | ............................... 197 | ............................... 198 | ......~........................ 199 | ............................... 200 | .........I..................... 201 | .........I.................I... 202 | ...~.....I..IIII...........I... 203 | ...........................I... 204 | ...........................I... 205 | ............................... 206 | ............................... 207 | .....~............~............ 208 | ............................... 209 | .I............................. 210 | .I............................. 211 | .I.......~..............~...... 212 | .I............................. 213 | .I............................. 214 | .I.....@....................... 215 | .I...___________________....... 216 | 217 | { 218 | "id": "crumble2", 219 | "music": "b", 220 | "sunSpeed": 30, 221 | "hi": "", 222 | "bye": "\"Inevitable\"? How foolish." 223 | } 224 | 225 | ............................... 226 | ............................... 227 | ............................... 228 | ............................... 229 | ......../...................... 230 | ................III............ 231 | ............................... 232 | ...I........................... 233 | ...I...................~....... 234 | ...I.......~................... 235 | ............................... 236 | ............................... 237 | ............................~.. 238 | ........~...................... 239 | ............................... 240 | ............................... 241 | ....................~.......... 242 | .III........................... 243 | ............................... 244 | ............~.................. 245 | ............................... 246 | .I....@........................ 247 | .I...____......~............... 248 | 249 | { 250 | "id": "crumble3", 251 | "music": "b", 252 | "sunSpeed": 30, 253 | "hi": "", 254 | "bye": "" 255 | } 256 | 257 | ............................... 258 | ............................... 259 | ............................... 260 | ............................... 261 | ............................... 262 | ......................=........ 263 | ............................... 264 | ............................... 265 | ............................... 266 | ....IIII....................... 267 | ............................... 268 | ............................... 269 | ............................... 270 | ............................... 271 | .I..........................I.. 272 | .I..........................I.. 273 | .I..........................I.. 274 | .I..........................I.. 275 | .I..........................I.. 276 | .I..........................I.. 277 | .I..........................I.. 278 | .I....@...n.................I.. 279 | .I...________.....____...____.. 280 | 281 | { 282 | "id": "button", 283 | "music": "c", 284 | "sunSpeed": 30, 285 | "hi": "A peculiar apparatus . . .", 286 | "bye": "" 287 | } 288 | 289 | ............................... 290 | ............................... 291 | ............................... 292 | ............................... 293 | ............................... 294 | ............................... 295 | ............................... 296 | ............................... 297 | ............................... 298 | ............................... 299 | ..................=............ 300 | ............................... 301 | ..~............................ 302 | ............................... 303 | .I............................. 304 | .I............................. 305 | .I............................. 306 | .I...~......................... 307 | .I........................n.... 308 | .I......................_____.. 309 | .I............................. 310 | .I.....@....................... 311 | .I.________..._______.......... 312 | 313 | { 314 | "id": "button1.5", 315 | "music": "c", 316 | "sunSpeed": 30, 317 | "hi": "This is like trying to ice skate uphill . . .", 318 | "bye": "" 319 | } 320 | 321 | ............................... 322 | ............................... 323 | ............................... 324 | ............................... 325 | ............................... 326 | .........................=..... 327 | ....IIIIII..................... 328 | ............................... 329 | ............................... 330 | ............................... 331 | .../........................... 332 | ............................... 333 | ............................... 334 | ............................... 335 | ............................... 336 | .I..........................I.. 337 | .I..........................I.. 338 | .I..........................I.. 339 | .I..........................I.. 340 | .I..........................I.. 341 | .I.............~............I.. 342 | .I..@.......~.....~......n..I.. 343 | .I.____........~......._____I.. 344 | 345 | { 346 | "id": "button2", 347 | "music": "c", 348 | "sunSpeed": 30, 349 | "hi": "", 350 | "bye": "" 351 | } 352 | 353 | ............................... 354 | ............................... 355 | ............................... 356 | ............................... 357 | ............................... 358 | .......IIIII......\............ 359 | ............................... 360 | ........................I...... 361 | ........................I...... 362 | ........................I...... 363 | ........................I...... 364 | ............................... 365 | ..........I......I............. 366 | ............................... 367 | ............................... 368 | .IIII...................IIIII.. 369 | .I..........................I.. 370 | .I..........................I.. 371 | .I..........................I.. 372 | .I..........................I.. 373 | .I..........................I.. 374 | .I....@.....................I.. 375 | .I__________________________I.. 376 | 377 | { 378 | "id": "dual", 379 | "music": "d", 380 | "sunSpeed": 30, 381 | "dualSun": true, 382 | "hi": "What is this fresh hell‽", 383 | "bye": "" 384 | } 385 | 386 | ............................... 387 | ............................... 388 | ............................... 389 | ............................... 390 | ............................... 391 | ..............II............... 392 | ............................... 393 | .........I..........I.......... 394 | ......I................I....... 395 | ...I......................I.... 396 | ............................... 397 | .I..........................I.. 398 | ............................... 399 | ............................... 400 | .I..........................I.. 401 | ............................... 402 | ............................... 403 | .I..........................I.. 404 | ............................... 405 | ............................... 406 | ............................... 407 | ..............@................ 408 | ..__________________________... 409 | 410 | { 411 | "id": "done", 412 | "music": "d", 413 | "sunSpeed": 80, 414 | "dualSun": true, 415 | "hi": "Thanks for playing.", 416 | "lastLevel": true 417 | } 418 | -------------------------------------------------------------------------------- /src/scaffolding/lib/props.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import { expandParticleProps } from "./particles"; 3 | import { expandTweenProps } from "./tweens"; 4 | import { expandTransitionProps } from "./transitions"; 5 | import { freezeStorage, removeAllFields, loadField } from "./store"; 6 | import { shaderProps } from "./shaders.js"; 7 | 8 | const savedChangedProps = loadField("changedProps", {}); 9 | export { savedChangedProps }; 10 | 11 | const rendererName = { 12 | [Phaser.AUTO]: "auto", 13 | [Phaser.CANVAS]: "canvas", 14 | [Phaser.WEBGL]: "webgl", 15 | }; 16 | 17 | const debug = !process.env.NODE_ENV || process.env.NODE_ENV === "development"; 18 | 19 | export function builtinPropSpecs( 20 | commands, 21 | shaderCoordFragments, 22 | shaderColorFragments 23 | ) { 24 | if (!debug) { 25 | Object.keys(commands).forEach((key) => { 26 | if (commands[key].debug) { 27 | delete commands[key]; 28 | } 29 | }); 30 | } 31 | 32 | return { 33 | "engine.time": [0.01, null, "loop.time"], 34 | "engine.frameTime": [0.01, null, "loop.delta"], 35 | "engine.actualFps": [0.01, null, "loop.actualFps"], 36 | "engine.targetFps": [0.01, null, "loop.targetFps"], 37 | "engine.renderer": [ 38 | rendererName[Phaser.AUTO], 39 | null, 40 | (scene, game) => rendererName[game.renderer.type], 41 | ], 42 | "engine.focused": [false, null, "scene.game.focused"], 43 | "engine.throttle": [false], 44 | "engine.stepping": [ 45 | false, 46 | (value, scene, game) => (value ? game.loop.sleep() : game.loop.wake()), 47 | ], 48 | "engine.step": [ 49 | (scene, game) => game.prop("engine.stepping") && game.loop.tick(), 50 | ], 51 | "engine.clearLocalStorage": [ 52 | () => { 53 | removeAllFields(); 54 | freezeStorage(); 55 | // eslint-disable-next-line no-self-assign 56 | window.location = window.location; 57 | }, 58 | ], 59 | "engine.clearGameState": [ 60 | () => { 61 | removeAllFields("game_"); 62 | freezeStorage(); 63 | // eslint-disable-next-line no-self-assign 64 | window.location = window.location; 65 | }, 66 | ], 67 | "engine.disableDebugUI": [(scene, game) => game.disableDebugUI()], 68 | 69 | "config.debug": [debug, null, () => debug], 70 | "config.width": [0, null, (scene, game) => game.config.width], 71 | "config.height": [0, null, (scene, game) => game.config.height], 72 | "config.tileWidth": [0, null, (scene, game) => game.config.tileWidth], 73 | "config.tileHeight": [0, null, (scene, game) => game.config.tileHeight], 74 | "config.xBorder": [0, null, (scene, game) => scene.xBorder], 75 | "config.yBorder": [0, null, (scene, game) => scene.xBorder], 76 | 77 | "scene.count": ["", null, "scene.scenes.length"], 78 | "scene.commandScenes": ["", null, (scene) => scene.command._scenes.size], 79 | "scene.class": ["", null, (scene) => scene.constructor.name], 80 | "scene.sceneId": ["", null, "sceneId"], 81 | "scene.parentSceneId": ["", null, "scene.settings.data.parentSceneId"], 82 | "scene.key": ["", null, "scene.key"], 83 | "scene.seed": ["", null, "scene.settings.data.seed"], 84 | "scene.scene_time": [0.01, null, "scene_time"], 85 | "scene.music": ["", null, "currentMusicName"], 86 | "scene.timeScale": [0.01, null, "timeScale"], 87 | "scene.shaderName": ["", null, "shaderName"], 88 | "scene.physicsFps": [0.01, null, "physics.world.fps"], 89 | "scene.images": [ 90 | 0, 91 | null, 92 | (scene) => 93 | scene.add.displayList.list.filter((node) => node.type === "Image") 94 | .length, 95 | ], 96 | "scene.sprites": [ 97 | 0, 98 | null, 99 | (scene) => 100 | scene.add.displayList.list.filter((node) => node.type === "Sprite") 101 | .length, 102 | ], 103 | "scene.particles": [ 104 | 0, 105 | null, 106 | (scene) => 107 | scene.add.displayList.list.filter( 108 | (node) => node.type === "ParticleEmitterManager" 109 | ).length, 110 | ], 111 | "scene.text": [ 112 | 0, 113 | null, 114 | (scene) => 115 | scene.add.displayList.list.filter((node) => node.type === "Text") 116 | .length, 117 | ], 118 | "scene.sounds": [0, null, "sounds.length"], 119 | "scene.timers": [0, null, "timers.length"], 120 | "scene.physicsColliders": [ 121 | 0, 122 | null, 123 | "physics.world.colliders._active.length", 124 | ], 125 | "scene.musicVolume": [ 126 | 1, 127 | 0, 128 | 1, 129 | (value, scene, game) => { 130 | game.changeVolume(game.volume); 131 | }, 132 | ], 133 | "scene.soundVolume": [ 134 | 1, 135 | 0, 136 | 1, 137 | (value, scene, game) => { 138 | game.changeVolume(game.volume); 139 | }, 140 | ], 141 | "scene.debugDraw": [ 142 | false, 143 | (value, scene) => { 144 | if (value) { 145 | scene.physics.world.createDebugGraphic(); 146 | } else { 147 | scene.physics.world.debugGraphic.destroy(); 148 | } 149 | }, 150 | ], 151 | "scene.replaceWithSelf": [(scene) => scene.replaceWithSelf(false)], 152 | 153 | "scene.camera.width": [0, null, "camera.width"], 154 | "scene.camera.height": [0, null, "camera.height"], 155 | "scene.camera.alpha": [0.1, null, "camera.alpha"], 156 | "scene.camera.zoom": [0.1, null, "camera.zoom"], 157 | "scene.camera.rotation": [0.1, null, "camera.rotation"], 158 | "scene.camera.x": [0, null, "camera.x"], 159 | "scene.camera.y": [0, null, "camera.y"], 160 | "scene.camera.scrollX": [0, null, "camera.scrollX"], 161 | "scene.camera.scrollY": [0, null, "camera.scrollY"], 162 | "scene.camera.centerX": [0, null, "camera.centerX"], 163 | "scene.camera.centerY": [0, null, "camera.centerY"], 164 | "scene.camera.boundsX": [0, null, "camera._bounds.x"], 165 | "scene.camera.boundsY": [0, null, "camera._bounds.y"], 166 | "scene.camera.boundsWidth": [0, null, "camera._bounds.width"], 167 | "scene.camera.boundsHeight": [0, null, "camera._bounds.height"], 168 | "scene.camera.useBounds": [true, null, "camera.useBounds"], 169 | 170 | "scene.camera.follow": [ 171 | "", 172 | null, 173 | objectIdentifier((scene) => scene.level, (scene) => scene.camera._follow), 174 | ], 175 | "scene.camera.followOffsetX": [0, null, "camera.followOffset.x"], 176 | "scene.camera.followOffsetY": [0, null, "camera.followOffset.y"], 177 | 178 | "scene.camera.lerp": [ 179 | 1, 180 | 0, 181 | 1, 182 | (value, scene) => { 183 | scene.setCameraLerp(); 184 | }, 185 | ], 186 | "scene.camera.deadzoneX": [ 187 | 0, 188 | 0, 189 | 1000, 190 | (value, scene) => { 191 | scene.setCameraDeadzone(); 192 | }, 193 | ], 194 | "scene.camera.deadzoneY": [ 195 | 0, 196 | 0, 197 | 1000, 198 | (value, scene) => { 199 | scene.setCameraDeadzone(); 200 | }, 201 | ], 202 | "scene.camera.hasBounds": [ 203 | true, 204 | (value, scene) => { 205 | scene.setCameraBounds(); 206 | }, 207 | ], 208 | 209 | "scene.trauma.amount": [0.01, null, "_trauma"], 210 | "scene.trauma.shakeAmount": [0.01, null, "_traumaShake"], 211 | "scene.trauma.decay": [0.001, 0, 1], 212 | "scene.trauma.exponent": [2.0, 0, 4], 213 | "scene.trauma.dx": [30.0, 0, 100], 214 | "scene.trauma.dy": [30.0, 0, 100], 215 | "scene.trauma.dt": [0.17, 0, 1], 216 | "scene.trauma.speed": [0.2, 0, 1], 217 | "scene.trauma.easeIn": [100, 0, 1000], 218 | "scene.trauma.legacy": [false], 219 | "scene.trauma.mild": [(scene) => scene.trauma(0.2)], 220 | "scene.trauma.minor": [(scene) => scene.trauma(0.5)], 221 | "scene.trauma.major": [(scene) => scene.trauma(0.8)], 222 | "scene.trauma.max": [(scene) => scene.trauma(100)], 223 | "scene.trauma.enabled": [true], 224 | 225 | ...commandKeyProps(commands), 226 | 227 | "command.gamepad.total": [0, null], 228 | "command.gamepad.A": [false, null], 229 | "command.gamepad.B": [false, null], 230 | "command.gamepad.X": [false, null], 231 | "command.gamepad.Y": [false, null], 232 | "command.gamepad.L1": [false, null], 233 | "command.gamepad.L2": [false, null], 234 | "command.gamepad.R1": [false, null], 235 | "command.gamepad.R2": [false, null], 236 | "command.gamepad.UP": [false, null], 237 | "command.gamepad.DOWN": [false, null], 238 | "command.gamepad.LEFT": [false, null], 239 | "command.gamepad.RIGHT": [false, null], 240 | "command.gamepad.LSTICKX": [0.01, null], 241 | "command.gamepad.LSTICKY": [0.01, null], 242 | "command.gamepad.RSTICKX": [0.01, null], 243 | "command.gamepad.RSTICKY": [0.01, null], 244 | 245 | "command.ignore_all.any": [ 246 | false, 247 | null, 248 | (scene) => scene.command.ignoreAll(), 249 | ], 250 | "command.ignore_all._transition": [ 251 | false, 252 | null, 253 | (scene) => scene.command.ignoreAll("_transition"), 254 | ], 255 | "command.ignore_all._sleep": [ 256 | false, 257 | null, 258 | (scene) => scene.command.ignoreAll("_sleep"), 259 | ], 260 | 261 | ...commandProps(commands), 262 | ...shaderProps(shaderCoordFragments, shaderColorFragments), 263 | }; 264 | } 265 | 266 | function commandProps(commands) { 267 | const props = {}; 268 | 269 | Object.entries(commands).forEach(([name, config]) => { 270 | props[`command.${name}.held`] = [false, null]; 271 | props[`command.${name}.started`] = [false, null]; 272 | props[`command.${name}.continued`] = [false, null]; 273 | props[`command.${name}.released`] = [false, null]; 274 | 275 | props[`command.${name}.heldFrames`] = [0, null]; 276 | props[`command.${name}.releasedFrames`] = [0, null]; 277 | props[`command.${name}.heldDuration`] = [0, null]; 278 | props[`command.${name}.releasedDuration`] = [0, null]; 279 | 280 | if (config.cooldown) { 281 | props[`command.${name}.cooldown`] = [config.cooldown, 0, 100000]; 282 | props[`command.${name}.coolingDown`] = [ 283 | false, 284 | null, 285 | (scene) => scene.command[name].coolingDown, 286 | ]; 287 | props[`command.${name}.coolingDownTime`] = [ 288 | 0.01, 289 | null, 290 | (scene) => scene.command[name].coolingDownTime, 291 | ]; 292 | } 293 | 294 | if (config.joystick) { 295 | props[`command.${name}.x`] = [ 296 | 0.01, 297 | null, 298 | (scene) => scene.command[name].held[0], 299 | ]; 300 | props[`command.${name}.y`] = [ 301 | 0.01, 302 | null, 303 | (scene) => scene.command[name].held[1], 304 | ]; 305 | } 306 | 307 | props[`command.${name}.enabled`] = [true]; 308 | 309 | if (config.execute) { 310 | const execute = 311 | typeof config.execute === "function" 312 | ? (scene, game) => config.execute(scene, game) 313 | : (scene, game) => scene[config.execute](scene, game); 314 | props[`command.${name}.execute`] = [execute]; 315 | } 316 | }); 317 | 318 | return props; 319 | } 320 | 321 | const knownInputs = [ 322 | "gamepad.A", 323 | "gamepad.B", 324 | "gamepad.X", 325 | "gamepad.Y", 326 | "gamepad.L1", 327 | "gamepad.L2", 328 | "gamepad.R1", 329 | "gamepad.R2", 330 | "gamepad.UP", 331 | "gamepad.DOWN", 332 | "gamepad.LEFT", 333 | "gamepad.RIGHT", 334 | "gamepad.LSTICK.UP", 335 | "gamepad.LSTICK.DOWN", 336 | "gamepad.LSTICK.LEFT", 337 | "gamepad.LSTICK.RIGHT", 338 | "gamepad.RSTICK.UP", 339 | "gamepad.RSTICK.DOWN", 340 | "gamepad.RSTICK.LEFT", 341 | "gamepad.RSTICK.RIGHT", 342 | "gamepad.LSTICK.RAW", 343 | "gamepad.RSTICK.RAW", 344 | 345 | ...Object.keys(Phaser.Input.Keyboard.KeyCodes).map((x) => `keyboard.${x}`), 346 | ].reduce((a, b) => { 347 | a[b] = true; 348 | return a; 349 | }, {}); 350 | 351 | export function keysWithPrefix(commands, prefix, skipWarning) { 352 | const keys = []; 353 | 354 | Object.entries(commands).forEach(([command, config]) => { 355 | if (!config.input) { 356 | return; 357 | } 358 | 359 | config.input.forEach((inputPath) => { 360 | if (!knownInputs[inputPath]) { 361 | if (!skipWarning) { 362 | // eslint-disable-next-line no-console 363 | console.error( 364 | `Unknown input path ${inputPath} for command ${command}` 365 | ); 366 | } 367 | return; 368 | } 369 | 370 | if (inputPath.startsWith(prefix)) { 371 | keys.push(inputPath.substr(prefix.length)); 372 | } 373 | }); 374 | }); 375 | 376 | return keys; 377 | } 378 | 379 | export function commandKeys(commands, skipWarning) { 380 | return keysWithPrefix(commands, "keyboard.", skipWarning); 381 | } 382 | 383 | export function gamepadKeys(commands, skipWarning) { 384 | return keysWithPrefix(commands, "gamepad.", skipWarning); 385 | } 386 | 387 | export function commandKeyProps(commands) { 388 | const props = {}; 389 | 390 | commandKeys(commands) 391 | .sort() 392 | .forEach((key) => { 393 | props[`command.keyboard.${key}`] = [false, null]; 394 | }); 395 | 396 | return props; 397 | } 398 | 399 | export function preprocessPropSpecs(propSpecs, particleImages) { 400 | expandParticleProps(propSpecs, particleImages); 401 | expandTweenProps(propSpecs, particleImages); 402 | expandTransitionProps(propSpecs, particleImages); 403 | } 404 | 405 | export function ManageableProps(propSpecs) { 406 | Object.entries(propSpecs).forEach(([key, spec]) => { 407 | if (!Array.isArray(spec)) { 408 | throw new Error( 409 | `Invalid spec for prop ${key}; expected array, got ${spec}` 410 | ); 411 | } 412 | 413 | let [value] = spec; 414 | // interject the scene and game, and wrap in a try 415 | if (typeof value === "function") { 416 | const original = value; 417 | value = () => { 418 | try { 419 | const { game } = window; 420 | const scene = game.topScene(); 421 | scene.command.recordPropExecution(key); 422 | original(scene, game); 423 | } catch (e) { 424 | // eslint-disable-next-line no-console 425 | console.error(e); 426 | } 427 | }; 428 | } 429 | 430 | if (key in savedChangedProps) { 431 | const [current, original] = savedChangedProps[key]; 432 | if (JSON.stringify(value) === JSON.stringify(original)) { 433 | value = current; 434 | } else { 435 | delete savedChangedProps[key]; 436 | } 437 | } 438 | 439 | this[key] = value; 440 | }); 441 | } 442 | 443 | export function makePropsWithPrefix(propSpecs, manageableProps) { 444 | if (debug) { 445 | return (prefix) => { 446 | const props = {}; 447 | Object.entries(manageableProps).forEach(([key, value]) => { 448 | if (key.startsWith(prefix)) { 449 | const name = key.substr(prefix.length); 450 | props[name] = value; 451 | } 452 | }); 453 | return props; 454 | }; 455 | } 456 | 457 | const cache = {}; 458 | return (prefix, clearCache) => { 459 | if (clearCache) { 460 | if (prefix) { 461 | delete cache[prefix]; 462 | } else { 463 | Object.keys(cache).forEach((key) => delete cache[key]); 464 | } 465 | } 466 | 467 | if (!cache[prefix]) { 468 | const props = {}; 469 | Object.entries(propSpecs).forEach(([key, spec]) => { 470 | if (key.startsWith(prefix)) { 471 | const [value] = spec; 472 | const name = key.substr(prefix.length); 473 | props[name] = value; 474 | } 475 | }); 476 | cache[prefix] = props; 477 | } 478 | return cache[prefix]; 479 | }; 480 | } 481 | 482 | export function PropLoader(propSpecs, manageableProps) { 483 | if (debug) { 484 | let p = manageableProps; 485 | return (name, update) => { 486 | if (update) { 487 | p = update; 488 | return; 489 | } 490 | 491 | if (!(name in p)) { 492 | throw new Error(`Invalid prop named ${name}`); 493 | } 494 | 495 | return p[name]; 496 | }; 497 | } 498 | 499 | return (name) => propSpecs[name][0]; 500 | } 501 | 502 | export function objectIdentifier(getContainer, getObject) { 503 | let cacheInput; 504 | let cacheOutput; 505 | return (...args) => { 506 | const object = getObject(...args); 507 | if (object === cacheInput) { 508 | return cacheOutput; 509 | } 510 | 511 | cacheInput = object; 512 | 513 | if (!object) { 514 | cacheOutput = object; 515 | return cacheOutput; 516 | } 517 | 518 | if (typeof object === "object" && object.name) { 519 | cacheOutput = object.name; 520 | return cacheOutput; 521 | } 522 | 523 | const container = getContainer(...args); 524 | if (container && typeof container === "object") { 525 | // eslint-disable-next-line guard-for-in, no-restricted-syntax 526 | for (const key in container) { 527 | const value = container[key]; 528 | 529 | if (value === object) { 530 | cacheOutput = key; 531 | return cacheOutput; 532 | } 533 | } 534 | } 535 | 536 | if (typeof object === "object" && object.texture && object.texture.key) { 537 | cacheOutput = object.texture.key; 538 | return cacheOutput; 539 | } 540 | 541 | cacheOutput = undefined; 542 | return cacheOutput; 543 | }; 544 | } 545 | --------------------------------------------------------------------------------